Skip to content

CORS in Express

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that blocks web pages from making requests to a different origin than the one that served them, unless the server explicitly allows it. This lesson covers configuring it correctly in Express.

Why CORS Exists

By default, browsers block JavaScript on one origin (e.g. https://myapp.com) from reading responses from a different origin (e.g. https://api.example.com), a protection called the Same-Origin Policy. CORS is the mechanism servers use to selectively opt back into allowing specific cross-origin requests.

This is purely a browser-enforced protection, server-to-server requests, curl, and tools like Postman are never blocked by CORS, only requests initiated from browser JavaScript are.

const cors = require('cors');

app.use(cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true,
}));

This configuration allows requests only from https://myapp.com, only for the listed methods, and allows cookies to be sent cross-origin via credentials: true.

cors() Configuration Options

cors({
  origin: string | string[] | RegExp | function,
  methods: ['GET', 'POST', ...],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: boolean,
});
  • origin: '*' allows any website to call your API, appropriate only for fully public, unauthenticated APIs.
  • origin can be a function for dynamically allowing an approved list of domains.
  • credentials: true is required if the client needs to send cookies cross-origin, and cannot be combined with origin: '*'.
  • Complex requests (like those with custom headers) trigger a browser "preflight" OPTIONS request, which cors() handles automatically.

CORS Cheatsheet

Common CORS configurations for different scenarios.

Scenario Configuration
Fully public API cors({ origin: '*' })
Single known frontend cors({ origin: 'https://myapp.com' })
Multiple known frontends cors({ origin: ['https://a.com', 'https://b.com'] })
Cookies/credentials required cors({ origin: 'https://myapp.com', credentials: true })
Dynamic allowlist cors({ origin: (origin, cb) => {...} })

A Dynamic Origin Allowlist

When you support multiple known frontend domains (e.g. staging and production), a function-based origin option checks the incoming request against an explicit allowlist.

const allowedOrigins = ['https://myapp.com', 'https://staging.myapp.com'];

app.use(cors({
  origin(origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      return callback(null, true);
    }
    callback(new Error('Not allowed by CORS'));
  },
  credentials: true,
}));

!origin allows requests with no Origin header at all, like server-to-server calls or same-origin requests.

Understanding Preflight Requests

For "non-simple" requests, using methods like PUT/DELETE, or custom headers like Authorization, browsers first send an automatic OPTIONS preflight request to check whether the actual request is allowed. The cors middleware responds to these automatically once registered, but they must reach your server unblocked by other middleware.

Common Mistakes

  • Using origin: '*' on an API that also accepts cookies or credentials, which browsers will reject outright.
  • Registering cors() after routes instead of before them, missing the preflight OPTIONS requests.
  • Assuming CORS protects your API from non-browser clients, it doesn't, curl and server-to-server calls bypass it entirely.
  • Hardcoding a single allowed origin when multiple environments (staging, production) need different ones.

Key Takeaways

  • CORS is a browser-enforced mechanism controlling which origins can read cross-origin responses.
  • It does not protect against non-browser clients, it's not a substitute for authentication.
  • credentials: true cannot be combined with a wildcard origin: '*'.
  • A function-based origin option supports a dynamic, environment-aware allowlist.

Pro Tip

Never treat a permissive CORS policy as a security control on its own, CORS decides which browsers can read your response, it does nothing to stop direct, authenticated or unauthenticated, requests from any other kind of client.