CORS
CORS — Cross-Origin Resource Sharing — is the browser mechanism that decides whether JavaScript on one origin may read responses from another. If your API at api.example.com is called by a frontend at app.example.com, the browser enforces CORS, and without the right response headers the call fails. The cors middleware sets those headers for you. The key to using it well is understanding what problem it solves — and what it does not.
The same-origin policy, and what "origin" means
An origin is the triple scheme + host + port. Two URLs share an origin only if all three match. By default browsers block cross-origin reads from JavaScript — CORS is how a server opts in to being read by specific origins.
URL pair | Same origin? |
|---|---|
| Yes (path is irrelevant) |
| No (scheme differs) |
| No (host differs) |
| No (port differs) |
Basic setup with the `cors` package
import cors from 'cors'
// Allow ALL origins — fine for a fully public, read-only API:
app.use(cors())
// Restrict to one trusted frontend (the common production case):
app.use(cors({ origin: 'https://app.example.com' }))Whitelisting multiple origins
const allowed = new Set([
'https://app.example.com',
'https://admin.example.com',
'http://localhost:5173', // dev frontend
])
app.use(cors({
origin(origin, cb) {
// Allow same-origin / non-browser requests (no Origin header):
if (!origin || allowed.has(origin)) return cb(null, true)
cb(new Error('Not allowed by CORS'))
},
}))Preflight requests (OPTIONS)
For "non-simple" requests — custom headers, methods like PUT/DELETE, or JSON content type — the browser first sends an automatic OPTIONS preflight asking permission. The cors middleware answers it automatically when mounted with app.use:
Browser preflight: OPTIONS /api/users Origin: https://app.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: content-type, authorization Server (cors middleware) replies: Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET,POST,PUT,DELETE Access-Control-Allow-Headers: content-type, authorization Access-Control-Max-Age: 86400 ← cache this preflight for a day
Credentials — cookies and auth headers
app.use(cors({
origin: 'https://app.example.com', // MUST be specific, not '*'
credentials: true, // allow cookies / Authorization
}))
// Browser side: fetch('https://api...', { credentials: 'include' })Common options
Option | Controls |
|---|---|
| Which origins are allowed (string, array, regex, or function) |
| Allowed HTTP methods (default GET,HEAD,PUT,PATCH,POST,DELETE) |
| Request headers the client may send |
| Response headers JS is allowed to read |
| Allow cookies / auth ( |
| Seconds the browser may cache the preflight |
Per-route CORS
// Public endpoint: open to all. Private endpoints: locked down.
app.get('/api/public', cors(), publicHandler)
const strict = cors({ origin: 'https://app.example.com', credentials: true })
app.use('/api/account', strict, accountRouter)