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". Here's how to fix it, ordered from "fastest" to "production-ready".
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 if you ever need credentials (cookies,Authorizationheader). - It opts you out of one of CORS's main protections — any site can now read your API.
- 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 // allow cookies / auth headers
}));
What each option does:
origin— 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 generously (600–7200 seconds) to avoid an OPTIONS round-trip per request.credentials: true— lets the browser send cookies/Authorizationwhen the client setscredentials: 'include'. Forces a specific origin (no*).
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 — without it, CDNs and browser caches will serve the wrong Access-Control-Allow-Origin to a different origin and you'll get silent intermittent CORS errors.
Per-route configuration
Most 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 don't get sent. Set
credentials: 'include'on the client'sfetch, setcredentials: trueon the server, set the cookie itself withSameSite=None; Secure. - The "wildcard with credentials" error. If you see "The value of 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'", switch from
*to a specific origin. - Custom
X-*headers blocked. List them inallowedHeaders. The default list does not include arbitrary custom headers. - Test with the same browser you ship to. Chrome and Firefox have small differences in preflight caching. Safari is even stricter.
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 open-source ours for self-host, and run a managed version at api.corsproxy.dev — free tier is 100 requests/day.
Skip the setup
Need to proxy a third-party API instead of configuring CORS yourself? Get a free key in 30 seconds.