Cross-Origin Resource Sharing

CORS is a browser feature, not a server guard. Your server always answers; CORS headers only tell the browser whether the page that asked may read the answer. curl 3,008 , a mobile app and a Node script ignore CORS entirely. That explains the two common mistakes: believing Access-Control-Allow-Origin protects data, and setting it to * on an API that uses cookies. The cors 6,195 package (MIT, expressjs/cors (https://github.com/expressjs/cors 6,195 ), 2.8.6) handles the preflight OPTIONS request and the headers on the real response. Prefer an allowlist function over a string: it can consult a database and lets through the Origin-less requests that server-to-server calls send.

An allowlist that supports credentialed requestsJavaScript
const allowed = new Set(['https://app.example.com', 'https://admin.example.com']);
app.use(cors({
  origin(origin, cb) {
    if (!origin || allowed.has(origin)) return cb(null, true);
    cb(null, false);                      // no header: the browser blocks the read
  },
  methods: ['GET', 'POST', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 600                             // cache the preflight for 10 minutes
}));
Output
OPTIONS /api/books  Origin: https://app.example.com  (+ the two request headers)
  204  access-control-allow-origin: https://app.example.com   access-control-max-age: 600
       access-control-allow-credentials: true   vary: Origin
       access-control-allow-headers: Content-Type,Authorization
       access-control-allow-methods: GET,POST,PATCH,DELETE
OPTIONS /api/books  Origin: https://evil.example
  200  allow: GET, HEAD, POST      (no access-control-* header at all)
GET     /api/books  Origin: https://evil.example
  200  {"items":["Dune"]}          (no access-control-* header at all)

Read the third response carefully: the refused origin still received the books. Without Access-Control-Allow-Origin a browser throws the body away before the page sees it - but the bytes crossed the network. CORS is not authorization. A private /api/books needs a token or session check (Cookies and Auth).

The refused preflight is informative too: with no CORS headers to add, it falls through to Express 24,430 's built-in OPTIONS responder, which answers 200 with an Allow list - so a 200 in your logs is not proof a preflight succeeded. Note also that credentials: true and origin: '*' are mutually exclusive by specification, and that vary: Origin stops a shared cache handing one origin's allow-origin header to another. Mount cors on the router that needs it, not the whole app: an /internal router without it is unreachable from any page.