Express makes CORS relatively painless, but there are a few gotchas that catch people off guard. Let me walk through every setup I’ve seen work in production.
The cors Package (Easiest Option)
npm install cors
The One-Liner (Development Only)
const cors = require('cors');
app.use(cors());
```text
This allows all origins, all methods, all headers. Fine for local development. Do NOT use this in production.
### Allow a Single Origin
```javascript
const cors = require('cors');
app.use(cors({
origin: 'https://myapp.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400,
}));
Allow Multiple Origins
This is where it gets slightly tricky. The cors package doesn’t accept an array for origin — it accepts a function:
const cors = require('cors');
const allowedOrigins = [
'https://myapp.com',
'https://admin.myapp.com',
'https://staging.myapp.com',
];
const corsOptions = {
origin: function (origin, callback) {
// Allow requests with no origin (like mobile apps, curl, Postman)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
};
app.use(cors(corsOptions));
```text
The `!origin` check is important. Some clients (mobile apps, server-to-server calls) don't send an `Origin` header. Without this check, those requests would fail.
### Dynamic Origin (Regex)
If you have many subdomains and don't want to list them all:
```javascript
const corsOptions = {
origin: function (origin, callback) {
if (!origin) return callback(null, true);
if (/\.myapp\.com$/.test(origin) || origin === 'https://myapp.com') {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
};
Per-Route CORS
Sometimes you want different CORS rules for different endpoints:
// Public API — any origin
app.get('/api/public', cors(), (req, res) => {
res.json({ message: 'This is public' });
});
// Authenticated API — specific origin + credentials
app.get('/api/private', cors({
origin: 'https://myapp.com',
credentials: true,
}), (req, res) => {
res.json({ message: 'This is private' });
});
// Webhook endpoint — no CORS needed
app.post('/api/webhooks/stripe', (req, res) => {
// No CORS config — this is server-to-server
res.sendStatus(200);
});
```text
### Enable CORS for Specific Routes Only
If you only want CORS on your API routes:
```javascript
app.use('/api', cors({
origin: 'https://myapp.com',
credentials: true,
}));
// These have CORS:
app.get('/api/users', ...);
app.post('/api/auth/login', ...);
// This does NOT have CORS:
app.get('/', ...);
Manual CORS (Without the Package)
If you don’t want another dependency, it’s not hard to do manually:
const allowedOrigins = ['https://myapp.com', 'https://admin.myapp.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Page');
res.header('Access-Control-Max-Age', '86400');
}
// Handle preflight
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
```text
This does the same thing as the cors package. Use whichever approach you prefer.
## CORS with Apollo GraphQL
```javascript
const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const cors = require('cors');
const corsOptions = {
origin: 'https://myapp.com',
credentials: true,
};
app.use('/graphql', cors(corsOptions), express.json());
app.use('/graphql', expressMiddleware(server));
CORS with Socket.IO
WebSockets have their own CORS handling. Socket.IO handles it through the cors option:
const { Server } = require('socket.io');
const io = new Server(httpServer, {
cors: {
origin: 'https://myapp.com',
methods: ['GET', 'POST'],
credentials: true,
},
});
```text
## Common Mistakes
**1. Using cors() globally in production.** This allows any website to call your API. If your API returns user data, any malicious site could access it (from the user's browser, with their cookies).
**2. Forgetting the OPTIONS handler.** If you're doing manual CORS, you MUST handle OPTIONS requests. Without it, preflight requests return a 404 or whatever your default handler does.
**3. Not setting Expose-Headers.** Your API returns custom headers like `X-Total-Count` for pagination, but your frontend can't read them. Add `Access-Control-Expose-Headers`.
**4. CORS + HTTPS mismatch.** Your frontend is on `https://myapp.com` but the origin in your CORS config is `http://myapp.com`. These are different origins. Match the protocol.
**5. Putting CORS on the client.** I've seen developers add CORS headers to their fetch calls. It doesn't work. CORS headers go on the SERVER response, not the client request.
## Verify Your Setup