Setting Up CORS in Node.js and Express: From Basic to Production-Ready

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());

This allows all origins, all methods, all headers. Fine for local development. Do NOT use this in production.

Allow a Single Origin#

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));

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:

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);
});

Enable CORS for Specific Routes Only#

If you only want CORS on your API routes:

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();
});

This does the same thing as the cors package. Use whichever approach you prefer.

CORS with Apollo GraphQL#

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,
  },
});

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#