How to enable CORS in Node.js and Express
Your Express API returns data fine from curl, but the browser blocks it with "No Access-Control-Allow-Origin header is present". The fixes below run from the fastest one-liner to a production-ready setup.
The one-line fix (development only)
Install the cors middleware and use it with default settings. It adds Access-Control-Allow-Origin: * to every response:
npm install cors
import express from 'express';
import cors from 'cors';
const app = express();
app.use(cors()); // Allows any origin: DEV ONLY
app.get('/api/users', (req, res) => res.json([{ id: 1, name: 'A' }]));
app.listen(3000);
Three caveats before you ship this to production:
- The wildcard
*is rejected by browsers once the client sends credentials, meaningcredentials: 'include'for cookies, TLS client certificates or browser-managed HTTP auth. AnAuthorizationheader you set yourself infetchdoesn't count; it works with*as long as it's inAccess-Control-Allow-Headers. - It opts you out of one of CORS's main protections: any site can now read your API's responses.
- If you have abuse-prone endpoints, this is the configuration attackers count on.
The right way: origin allowlist
For production, reflect a specific origin from a known allowlist. The cors middleware accepts a function:
const ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
'http://localhost:3000' // dev only
]);
app.use(cors({
origin: (origin, callback) => {
// Same-origin / curl / mobile requests have no Origin header.
if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.has(origin)) return callback(null, origin);
return callback(new Error(`Origin ${origin} not allowed`));
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
exposedHeaders: ['X-RateLimit-Remaining'],
maxAge: 600, // browser caches preflight for 10 min
credentials: true // only needed if the client sends cookies
}));
What each option does:
origin: a callback that decides per-request. Passing a string to the callback echoes that asAccess-Control-Allow-Origin.methods/allowedHeaders: what the preflight response advertises as permitted.exposedHeaders: extra response headers the browser is allowed to expose to JS (most are hidden by default).maxAge: how long the browser caches the preflight result. Set it generously (600–7200 seconds) to avoid an OPTIONS round-trip per request.credentials: true: sendsAccess-Control-Allow-Credentials: true, which lets the page read responses to credentialed requests (credentials: 'include', i.e. cookies). It requires a specific origin rather than*. You don't need it for a bearer token sent in anAuthorizationheader; listingAuthorizationinallowedHeadersis enough.
Without the cors package
If you don't want a dependency, the same behavior is ~10 lines:
const ALLOWED = new Set([
'https://app.example.com',
'http://localhost:3000'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOWED.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '600');
}
// Short-circuit OPTIONS preflight before route handlers see it.
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
Don't forget Vary: Origin when you echo the request's origin. Without it, a CDN or browser cache can serve a response carrying one origin's Access-Control-Allow-Origin to a different origin, and you get intermittent CORS errors that are hard to reproduce. The cors package adds it for you when it reflects an origin; hand-written middleware has to set it.
Per-route configuration
Many APIs want different rules per route. A public health endpoint can be *, but the auth endpoints need a strict allowlist:
const publicCors = cors();
const strictCors = cors({
origin: ['https://app.example.com'],
credentials: true
});
app.get('/health', publicCors, (req, res) => res.send('ok'));
app.use('/api/auth', strictCors);
app.use('/api/data', strictCors);
Common gotchas
- Preflight returns 404. Your CORS middleware comes after the route. Always register CORS first.
- Cookies, the wildcard-with-credentials error, and
SameSite. These are covered in CORS with cookies and credentials. - Custom
X-*headers blocked. List them inallowedHeaders. The default list does not include arbitrary custom headers. - Preflight caching differs by browser. Chrome caps
Access-Control-Max-Ageat 7200 seconds and Firefox at 86400, so a largermaxAgewon't do more in Chrome.
When you don't control the upstream
If the API you're hitting isn't yours and won't add CORS headers, run it through a CORS proxy. The proxy sends the headers the browser needs. We run a managed one at api.corsproxy.dev (the free tier is 500 requests/day), and publish a separate, simpler open-source relay you can self-host.
Skip the setup
Need to proxy a third-party API instead of configuring CORS yourself? Get a free key.