Home JavaScript CORS in Node.js and Express — The Complete Guide
Intermediate 7 min · 2026-07-12

CORS in Node.js and Express — The Complete Guide

CORS in Node.js and Express explained: same-origin policy, preflight requests, allowed origins, credentials, and troubleshooting CORS errors in production APIs..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 15-20 minutes
  • Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
 ● Production Incident
Quick Answer

Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls which origins can access server resources. In Express, the cors middleware package handles CORS headers (Access-Contr

✦ Definition~90s read
What is CORS in Node.js and Express?

Cross-Origin Resource Sharing (CORS) is a browser security mechanism that controls which origins can access server resources. In Express, the cors middleware package handles CORS headers (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers).

Imagine you live in a gated community with a strict security guard.

For production APIs, CORS must be configured with specific allowed origins (not wildcard '*' for credentialed requests), proper handling of preflight OPTIONS requests, and explicit exposure of custom headers. Misconfigured CORS is one of the most common causes of 'frontend works locally but not in production' bugs.

Plain-English First

Imagine you live in a gated community with a strict security guard. Your friend from another neighborhood wants to visit you. The guard checks their ID and says, 'Sorry, I can't let you in because you're not from here.' That's the browser's same-origin policy. CORS is like giving your friend a special pass signed by your community's manager. The guard sees the pass and lets them in. In web terms, the 'pass' is a special HTTP header (Access-Control-Allow-Origin) that your server sends to tell the browser, 'It's okay, I trust this other site.'

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

Your React app fetches data from your Node.js API on localhost:3001, but localhost:5173 refuses the response with a cryptic CORS error. You add app.use(cors()) and everything works locally. In production, the same error appears because your API is on api.example.com and your front end is on app.example.com. CORS errors are the most common integration issue in modern web development, and the fix is rarely as simple as 'install the cors package'. This article covers exactly how CORS works, how to configure it correctly for production, and how to debug the three most common CORS failures.

What Is CORS and Why Does It Exist?

CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls how web pages can request resources from a different origin (protocol, domain, or port). Without CORS, a malicious site could read sensitive data from another site using the user's credentials. The browser enforces the same-origin policy by default, blocking cross-origin requests unless the server explicitly allows them via CORS headers. For Node.js/Express apps, this means your API must respond with the correct headers to be accessible from frontend apps hosted elsewhere. Understanding CORS is critical because misconfiguration can either block legitimate traffic or open security holes. The core header is Access-Control-Allow-Origin, which specifies which origins are permitted. Other headers control methods, headers, credentials, and caching. In production, you'll often need to handle preflight requests (OPTIONS) for non-simple requests like those with custom headers or PUT/DELETE methods.

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
const express = require('express');
const app = express();

app.get('/data', (req, res) => {
  res.json({ message: 'Hello from server' });
});

app.listen(3000, () => console.log('Server running on port 3000'));
Output
Server running on port 3000
Try it live
🔥CORS is browser-only
CORS is enforced by browsers, not by servers or tools like curl. You can test CORS issues by making requests from a browser's dev tools or using a frontend app.
📊 Production Insight
In production, never use Access-Control-Allow-Origin: * with credentials (cookies, auth headers). This is a common misconfiguration that exposes user data.
🎯 Key Takeaway
CORS is a browser security feature that restricts cross-origin requests; servers must opt-in via headers.
cors-nodejs THECODEFORGE.IO CORS Middleware Architecture in Express Layered components from client to server security Client Layer Browser | Fetch/XMLHttpRequest Network Layer HTTP Request | Origin Header Middleware Layer cors Package | Custom Handler Configuration Layer Origin Whitelist | Allowed Methods | Credentials Flag Response Layer CORS Headers | Preflight Response Security Layer Origin Validation | Credential Check THECODEFORGE.IO
thecodeforge.io
Cors Nodejs

Setting Up CORS in Express with the `cors` Package

The easiest way to add CORS to an Express app is using the cors npm package. Install it with npm install cors. Then, you can apply it globally to all routes or per-route. The simplest usage is app.use(cors()), which allows all origins, methods, and headers. This is fine for development but dangerous for production. For production, configure specific origins, methods, and allowed headers. The cors middleware automatically handles preflight requests. You can also enable credentials (cookies, authorization headers) with credentials: true. However, when credentials are enabled, you cannot use a wildcard origin; you must specify exact origins. The middleware also supports options like maxAge to cache preflight responses, reducing network overhead. Always place the CORS middleware before your route handlers to ensure headers are set on all responses, including errors.

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
  origin: 'https://myfrontend.com',
  methods: 'GET,POST,PUT,DELETE',
  allowedHeaders: 'Content-Type,Authorization',
  credentials: true,
  optionsSuccessStatus: 200
};

app.use(cors(corsOptions));

app.get('/data', (req, res) => {
  res.json({ message: 'CORS configured!' });
});

app.listen(3000);
Output
Server running on port 3000
Try it live
⚠ Credentials and wildcard origins don't mix
If you set credentials: true, you must specify exact origins in origin. Using * will cause the browser to reject the request.
📊 Production Insight
A common production failure is forgetting to handle preflight for routes that require authentication. Ensure your OPTIONS handler returns 200 with appropriate headers, or the browser will block the actual request.
🎯 Key Takeaway
Use the cors package for quick setup, but always restrict origins in production.

Manual CORS Configuration Without External Packages

If you prefer minimal dependencies or need fine-grained control, you can implement CORS manually using Express middleware. This involves setting the Access-Control-Allow-Origin header and optionally other headers like Access-Control-Allow-Methods and Access-Control-Allow-Headers. You must also handle preflight requests by responding to OPTIONS requests with a 200 status and the appropriate headers. Manual configuration gives you the ability to dynamically set the allowed origin based on the request's Origin header, which is useful for multiple allowed origins. However, be careful with wildcard origins when credentials are involved. Manual implementation also allows you to log or audit CORS requests. The downside is more boilerplate and potential for mistakes, such as forgetting to set the Vary: Origin header, which can cause caching issues in CDNs.

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const express = require('express');
const app = express();

const allowedOrigins = ['https://frontend1.com', 'https://frontend2.com'];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
  }
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Vary', 'Origin');

  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

app.get('/data', (req, res) => {
  res.json({ message: 'Manual CORS works!' });
});

app.listen(3000);
Output
Server running on port 3000
Try it live
💡Always set Vary: Origin
When dynamically setting the allowed origin, include Vary: Origin to prevent CDNs from caching responses for the wrong origin.
📊 Production Insight
A missing Vary: Origin header can cause a CDN to serve a cached response with a mismatched Access-Control-Allow-Origin, breaking CORS for some users.
🎯 Key Takeaway
Manual CORS gives full control but requires careful handling of preflight and caching headers.
cors-nodejs THECODEFORGE.IO CORS Middleware Architecture in Express Layered components from client to server response Client Layer Browser | Fetch/XMLHttpRequest | Origin Header Network Layer HTTP Request | Preflight OPTIONS | Credentials Flag Express Middleware Stack cors() Package | Custom Middleware | Dynamic Whitelist CORS Configuration Allowed Origins | Allowed Methods | Allowed Headers Response Layer Access-Control-* Headers | Credentials Support | Error Handling THECODEFORGE.IO
thecodeforge.io
Cors Nodejs

Handling Preflight Requests Correctly

Preflight requests are OPTIONS requests sent by the browser before certain cross-origin requests (e.g., those with custom headers, non-simple methods like PUT/DELETE, or when credentials are included). The server must respond with the allowed methods, headers, and origin. If the preflight fails (e.g., missing headers or wrong status), the browser blocks the actual request. In Express, the cors package handles preflight automatically. For manual setup, you must explicitly check for OPTIONS and return 200 with the CORS headers. A common mistake is not handling preflight for all routes, especially those behind authentication middleware. Ensure your CORS middleware runs before any auth middleware, or the preflight will be rejected. Also, set Access-Control-Max-Age to cache preflight responses, reducing latency for subsequent requests.

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const express = require('express');
const app = express();

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://myfrontend.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  res.setHeader('Access-Control-Max-Age', '86400'); // 24 hours

  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

app.get('/data', (req, res) => {
  res.json({ message: 'Preflight handled' });
});

app.listen(3000);
Output
Server running on port 3000
Try it live
⚠ Preflight and auth middleware order
Place CORS middleware before authentication middleware. Otherwise, preflight requests will be rejected due to missing auth headers.
📊 Production Insight
In production, set Access-Control-Max-Age to a reasonable value (e.g., 86400 seconds) to reduce preflight requests. Too short a value increases latency; too long can delay policy updates.
🎯 Key Takeaway
Preflight requests must be handled with a 200 response and appropriate CORS headers before any other middleware.

CORS with Credentials: Cookies and Authorization Headers

When your frontend needs to send cookies or HTTP authentication (e.g., Bearer tokens in Authorization header), you must enable credentials in CORS. This requires setting Access-Control-Allow-Credentials: true and the Access-Control-Allow-Origin header must not be a wildcard; it must be the exact origin. Additionally, the frontend must set withCredentials: true on XMLHttpRequest or credentials: 'include' on fetch. On the server, if using cookies, ensure the cookie's SameSite attribute is set appropriately (e.g., None for cross-origin, but requires Secure). A common pitfall is forgetting to set credentials: true on both sides, resulting in the browser not sending cookies. Also, be aware that some CDNs or proxies may strip the Access-Control-Allow-Credentials header if not configured correctly.

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({
  origin: 'https://myfrontend.com',
  credentials: true
}));

app.get('/profile', (req, res) => {
  // Assuming session middleware sets req.user
  if (req.user) {
    res.json({ user: req.user });
  } else {
    res.status(401).json({ error: 'Not authenticated' });
  }
});

app.listen(3000);
Output
Server running on port 3000
Try it live
💡Frontend must opt-in to send credentials
On the client side, use fetch(url, { credentials: 'include' }) or set xhr.withCredentials = true.
📊 Production Insight
If your API uses cookies for auth, ensure the cookie's SameSite attribute is None and Secure is true for cross-origin requests. Otherwise, modern browsers will block the cookie.
🎯 Key Takeaway
Credentials require explicit opt-in on both server (credentials: true) and client (withCredentials), and origin must be exact.

Dynamic Origin Whitelisting for Multi-Tenant APIs

Many production APIs serve multiple frontend origins (e.g., different subdomains or customer-specific domains). Hardcoding origins is impractical. Instead, implement dynamic origin validation: check the request's Origin header against a whitelist (e.g., from environment variables or a database). If the origin is allowed, set Access-Control-Allow-Origin to that origin; otherwise, omit the header or set it to the requesting origin (which will cause the browser to block). Always include the Vary: Origin header to prevent caching issues. For performance, cache the whitelist in memory and refresh periodically. Be cautious with regex-based matching to avoid open redirect vulnerabilities. A common mistake is using a regex that is too permissive (e.g., *.example.com can match evil.example.com.attacker.com).

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express = require('express');
const app = express();

const allowedOrigins = process.env.ALLOWED_ORIGINS
  ? process.env.ALLOWED_ORIGINS.split(',')
  : ['https://app1.example.com', 'https://app2.example.com'];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
    res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
    res.setHeader('Vary', 'Origin');
  }
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200);
  }
  next();
});

app.get('/data', (req, res) => {
  res.json({ message: 'Dynamic origin' });
});

app.listen(3000);
Output
Server running on port 3000
Try it live
🔥Use environment variables for whitelist
Store allowed origins in environment variables to avoid hardcoding and enable easy updates without redeployment.
📊 Production Insight
A common failure is not handling the case where the Origin header is missing (e.g., server-to-server requests). In such cases, you may want to allow the request without CORS headers or reject it based on other criteria.
🎯 Key Takeaway
Dynamic origin whitelisting scales for multi-tenant apps but requires careful validation and caching.

Debugging CORS Issues: Tools and Techniques

CORS errors can be frustrating because the browser often provides vague messages like 'No 'Access-Control-Allow-Origin' header is present'. To debug, first check the network tab in DevTools: look for the OPTIONS preflight request and the actual request. Verify the response headers include the expected CORS headers. If the preflight fails, check the status code (should be 200) and headers. Use curl to simulate requests: curl -H "Origin: https://myfrontend.com" -I http://localhost:3000/data to see response headers. For more advanced debugging, use tools like cors-test or online CORS testers. Common issues include: missing headers, wrong origin (including trailing slash), credentials mismatch, and preflight not handled. Also, check if the browser's cache is causing stale preflight responses; clear the cache or disable caching in DevTools.

terminalBASH
1
2
3
4
5
# Simulate a CORS request with curl
curl -H "Origin: https://myfrontend.com" \
  -H "Access-Control-Request-Method: GET" \
  -X OPTIONS \
  -v http://localhost:3000/data 2>&1 | grep -i "access-control"
Output
< access-control-allow-origin: https://myfrontend.com
< access-control-allow-methods: GET,POST,PUT,DELETE
< access-control-allow-credentials: true
💡Use curl to test without browser
curl is your best friend for CORS debugging. It shows exactly what headers the server returns, bypassing browser quirks.
📊 Production Insight
In production, enable CORS logging on your server to capture failed requests. This helps identify if a legitimate client is being blocked due to a misconfigured origin whitelist.
🎯 Key Takeaway
Debug CORS by inspecting network headers, using curl, and checking for common misconfigurations.

CORS and Security: Avoiding Common Pitfalls

CORS is not a security mechanism per se; it's a way for servers to relax the same-origin policy. Misconfigurations can lead to vulnerabilities. Never use Access-Control-Allow-Origin: * with credentials. Avoid reflecting the Origin header without validation (e.g., res.setHeader('Access-Control-Allow-Origin', req.headers.origin)) as this allows any site to make credentialed requests. Be cautious with Access-Control-Allow-Methods and Access-Control-Allow-Headers: only allow what your API actually uses. For example, if you don't support PUT, don't include it. Also, consider using Access-Control-Expose-Headers to control which headers the browser can access. Finally, remember that CORS only applies to browser requests; server-to-server or mobile app requests are not restricted. Therefore, always implement proper authentication and authorization on your API regardless of CORS.

server.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require('express');
const cors = require('cors');
const app = express();

// Insecure: reflecting origin without validation
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
  res.setHeader('Access-Control-Allow-Credentials', 'true');
  next();
});

// Secure: validate origin against whitelist
const allowedOrigins = ['https://trusted.com'];
app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin);
    res.setHeader('Access-Control-Allow-Credentials', 'true');
  }
  next();
});

app.listen(3000);
Output
Server running on port 3000
Try it live
⚠ Never reflect origin without validation
Reflecting the Origin header without checking it against a whitelist allows any site to make credentialed requests, leading to CSRF-like attacks.
📊 Production Insight
A real-world attack: an API that reflected origin allowed an attacker's site to make authenticated requests on behalf of users, exfiltrating data. Always whitelist origins.
🎯 Key Takeaway
CORS is a relaxation of security; always validate origins and restrict methods/headers to what's necessary.

CORS in Production: Reverse Proxies, CDNs, and Caching

In production, your Express app often sits behind a reverse proxy (e.g., Nginx) or a CDN (e.g., Cloudflare). These intermediaries can add or modify CORS headers. Ensure your proxy is configured to pass through or set the correct CORS headers. For example, Nginx can add headers with add_header. CDNs may cache responses, so the Vary: Origin header is crucial to serve different CORS headers per origin. If your CDN strips the Vary header, you may serve a cached response with a wrong Access-Control-Allow-Origin. Also, if you use HTTPS termination at the proxy, ensure the Origin header is preserved. Another consideration: if your API is behind a CDN that caches responses, preflight requests may not be cached unless you set Access-Control-Max-Age and configure the CDN to cache OPTIONS responses.

nginx.confNGINX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server {
    listen 443 ssl;
    server_name api.example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header Origin $http_origin;

        # Add CORS headers if backend doesn't set them
        add_header Access-Control-Allow-Origin $http_origin always;
        add_header Access-Control-Allow-Credentials true always;
        add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,OPTIONS' always;
        add_header Access-Control-Allow-Headers 'Content-Type,Authorization' always;
        add_header Vary 'Origin' always;

        if ($request_method = OPTIONS) {
            return 204;
        }
    }
}
🔥Proxy headers must be forwarded
Ensure your reverse proxy forwards the Origin header to your backend. Otherwise, your Express app may see no origin and fail to set CORS headers.
📊 Production Insight
A common production issue: CDN caching causes CORS errors for users on different origins because the cached response has a fixed Access-Control-Allow-Origin. Always set Vary: Origin and ensure the CDN respects it.
🎯 Key Takeaway
In production, reverse proxies and CDNs can interfere with CORS; configure them to preserve or set headers correctly.

Testing CORS Configuration Automatically

Automated testing of CORS is essential to catch regressions. You can write integration tests that simulate cross-origin requests using libraries like supertest with custom headers. Test both preflight (OPTIONS) and actual requests. Verify that allowed origins receive the correct headers, disallowed origins do not, and that credentials work when enabled. Also test edge cases: missing Origin header, multiple origins, and caching behavior. For CI/CD, include these tests in your pipeline. A simple test suite can use chai or jest to assert response headers. Remember to test with different HTTP methods and custom headers to ensure preflight is handled. Automated testing prevents accidental misconfigurations from reaching production.

test/cors.test.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const request = require('supertest');
const app = require('../app');

describe('CORS', () => {
  it('should allow allowed origin', async () => {
    const res = await request(app)
      .get('/data')
      .set('Origin', 'https://allowed.com');
    expect(res.headers['access-control-allow-origin']).toBe('https://allowed.com');
  });

  it('should not allow disallowed origin', async () => {
    const res = await request(app)
      .get('/data')
      .set('Origin', 'https://evil.com');
    expect(res.headers['access-control-allow-origin']).toBeUndefined();
  });

  it('should handle preflight', async () => {
    const res = await request(app)
      .options('/data')
      .set('Origin', 'https://allowed.com')
      .set('Access-Control-Request-Method', 'GET');
    expect(res.status).toBe(200);
    expect(res.headers['access-control-allow-methods']).toContain('GET');
  });
});
Output
PASS test/cors.test.js
CORS
✓ should allow allowed origin (15ms)
✓ should not allow disallowed origin (10ms)
✓ should handle preflight (12ms)
Try it live
💡Test both allowed and disallowed origins
Always include negative tests to ensure disallowed origins are blocked. This catches accidental wildcard or reflection issues.
📊 Production Insight
A production outage was caused by a developer accidentally removing the origin whitelist check in a refactor. Automated tests caught it before deployment.
🎯 Key Takeaway
Automated CORS tests prevent regressions and ensure your configuration works as expected across deployments.

Complete Origin Option Reference

The origin option in the cors package accepts multiple types: Boolean, String, RegExp, Array, and Function. Setting origin: true mirrors the request's Origin header in the response's Access-Control-Allow-Origin. origin: false disables CORS (useful for same-origin only). A string sets a single origin (e.g., 'https://example.com'). A RegExp matches origins (e.g., /https:\/\/.\.example\.com$/). An array allows multiple origins or patterns (e.g., ['https://a.com', /https:\/\/.\.b\.com$/]). A function enables dynamic per-request logic, receiving the request origin and a callback. Always validate the origin server-side to prevent open CORS.

cors-origin-examples.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const cors = require('cors');

// Boolean
app.use(cors({ origin: true })); // echo origin

// String
app.use(cors({ origin: 'https://example.com' }));

// RegExp
app.use(cors({ origin: /https:\/\/.*\.example\.com$/ }));

// Array
app.use(cors({ origin: ['https://a.com', /https:\/\/.*\.b\.com$/] }));

// Function
app.use(cors({
  origin: function (origin, callback) {
    if (!origin || whitelist.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));
Output
No output; configures CORS middleware.
Try it live
📊 Production Insight
In production, never use origin: true unless your API is truly public. Always validate origins against a whitelist to prevent unauthorized access.
🎯 Key Takeaway
Use the simplest origin type that meets your needs. Prefer explicit strings or RegExp over arrays for clarity. Use the function form only when dynamic logic is unavoidable.

Methods, AllowedHeaders, ExposedHeaders, Credentials, and MaxAge Options

Beyond origin, the cors package provides fine-grained control. methods sets allowed HTTP methods for preflight (default: GET,HEAD,PUT,PATCH,POST,DELETE). allowedHeaders specifies which headers can be sent in the actual request (default: the request's Access-Control-Request-Headers). exposedHeaders lists headers the browser exposes to JavaScript (e.g., X-Total-Count). credentials enables cookies and authorization headers when true. maxAge caches the preflight response in seconds, reducing OPTIONS requests. Set maxAge to a high value (e.g., 86400) for stable configurations. For credentials, the origin must be explicit (not *). Expose only necessary headers to minimize attack surface.

cors-options.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
const corsOptions = {
  origin: 'https://example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  exposedHeaders: ['X-Total-Count'],
  credentials: true,
  maxAge: 86400 // 24 hours
};

app.use(cors(corsOptions));
Output
No output; configures CORS middleware.
Try it live
📊 Production Insight
For APIs behind a CDN, set maxAge to at least 600 seconds to reduce preflight requests. Monitor preflight traffic to tune the value.
🎯 Key Takeaway
Explicitly set methods and allowedHeaders to the minimum required. Use maxAge to reduce preflight overhead. Enable credentials only when needed and never with origin: *.

Dynamic CORS Per-Request via Function

The origin option can be a function that dynamically determines the allowed origin per request. This is essential for multi-tenant APIs where each tenant has a different allowed origin. The function receives the request's origin header and a callback. Use it to query a database or check a whitelist. The callback follows the pattern callback(error, originValue). Pass true to allow the origin, a string to override, or false to block. For performance, cache the whitelist in memory and invalidate periodically. Avoid synchronous operations inside the function; use async/await with the callback pattern.

dynamic-cors.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const cors = require('cors');

const allowedOrigins = new Map([
  ['tenant1', 'https://tenant1.example.com'],
  ['tenant2', 'https://tenant2.example.com']
]);

app.use(cors({
  origin: async function (origin, callback) {
    // Simulate async lookup
    const tenant = await getTenantFromOrigin(origin);
    if (tenant && allowedOrigins.has(tenant)) {
      callback(null, allowedOrigins.get(tenant));
    } else {
      callback(null, false);
    }
  }
}));

async function getTenantFromOrigin(origin) {
  // e.g., parse subdomain
  const match = origin.match(/https:\/\/(.*)\.example\.com/);
  return match ? match[1] : null;
}
Output
No output; configures CORS middleware.
Try it live
📊 Production Insight
Implement a fallback: if the origin is missing (e.g., server-to-server requests), allow it by passing true or a default origin. Log denied origins for debugging.
🎯 Key Takeaway
Use the function form for dynamic CORS when origins are not known at startup. Keep the lookup fast and cache results to avoid latency.

CORS Error Handling and Vary Header Behavior

When CORS fails, the browser blocks the request and logs an error. The server should not send a 200 with missing CORS headers; instead, it should return an appropriate error. The cors package emits an error on invalid origin. Use Express error-handling middleware to catch it and respond with a 403. Additionally, the Vary: Origin header tells caches that the response varies based on the request origin. The cors package automatically sets Vary: Origin when the origin is dynamic. For static origins, it sets Vary: Origin only if credentials are enabled. Always ensure Vary is set correctly to prevent cache poisoning.

cors-error-handling.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const cors = require('cors');

app.use(cors({
  origin: function (origin, callback) {
    if (!origin || whitelist.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  }
}));

// Error handling middleware
app.use((err, req, res, next) => {
  if (err.message === 'Not allowed by CORS') {
    res.status(403).json({ error: 'CORS origin not allowed' });
  } else {
    next(err);
  }
});
Output
On CORS error: HTTP 403 with JSON body.
Try it live
📊 Production Insight
Use a reverse proxy like nginx to add Vary: Origin if your application server doesn't. Monitor CORS errors via logs to detect misconfigured clients.
🎯 Key Takeaway
Always handle CORS errors explicitly to return meaningful HTTP status codes. Ensure the Vary header includes Origin when the response depends on it.
cors Package vs Manual CORS Configuration Trade-offs in setup, control, and maintenance cors Package Manual Configuration Setup Complexity Minimal: app.use(cors()) Manual: set headers per route Preflight Handling Automatic OPTIONS response Manual OPTIONS route required Dynamic Origins Callback function for origin check Custom logic in middleware Credentials Support Set credentials: true option Set Access-Control-Allow-Credentials hea Debugging Ease Built-in logging with debug option Manual logging via console Security Pitfalls Avoids wildcard with credentials Risk of misconfigured origins THECODEFORGE.IO
thecodeforge.io
Cors Nodejs

CORS Behind Reverse Proxies (Nginx, CloudFront)

When your Node.js app runs behind a reverse proxy (nginx, CloudFront), the Origin header may be modified or stripped. Ensure your proxy forwards the Origin header. For nginx, add proxy_set_header Origin $http_origin;. For CloudFront, whitelist the Origin header in the cache behavior. The proxy may also handle preflight requests (OPTIONS) directly. Configure nginx to respond with CORS headers for OPTIONS to offload your app. For CloudFront, use custom error responses or Lambda@Edge. Always test with curl -I -X OPTIONS to verify headers. Remember that the Vary header should include Origin to avoid serving cached responses to wrong origins.

nginx-cors.confNGINX
1
2
3
4
5
6
7
8
9
10
11
12
13
location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' 86400;
        add_header 'Content-Length' 0;
        return 204;
    }
    proxy_pass http://node_app;
    proxy_set_header Origin $http_origin;
    proxy_set_header Host $host;
}
Output
No output; nginx configuration.
📊 Production Insight
For CloudFront, create a custom error response for 403 to add CORS headers, or use Lambda@Edge for dynamic CORS. Monitor preflight requests to optimize caching.
🎯 Key Takeaway
Offload preflight handling to the reverse proxy when possible. Ensure the Origin header is forwarded correctly. Test with curl to confirm headers.
● Production incidentPOST-MORTEMseverity: high

CORS Misconfiguration Causes Silent Data Leak in Multi-Tenant SaaS

Symptom
Users reported seeing other tenants' data in their dashboards after a new frontend deployment.
Assumption
The backend correctly validates authentication and authorization per request; CORS is just a browser policy and doesn't affect server-side security.
Root cause
The server was configured to reflect the Origin header (Access-Control-Allow-Origin: ${origin}) and allowed credentials (Access-Control-Allow-Credentials: true) without validating the origin against an allowlist. An attacker could set any Origin header, and the browser would accept the response, exposing sensitive data.
Fix
Implemented a strict allowlist of allowed origins in the CORS middleware. For each request, the server checks if the Origin is in the allowlist; if not, it returns a 403. Also added Vary: Origin header to prevent caching issues. Removed origin reflection entirely.
Key lesson
  • Never reflect the Origin header without validation; always use an allowlist.
  • CORS is a browser-enforced policy, but misconfigurations can lead to data leaks if credentials are allowed.
  • Always test CORS with multiple origins, including unexpected ones, during QA.
  • Use tools like curl to verify server responses for different origins.
⚙ Quick Reference
9 commands from this guide
FileCommand / CodePurpose
server.jsconst express = require('express');What Is CORS and Why Does It Exist?
terminalcurl -H "Origin: https://myfrontend.com" \Debugging CORS Issues
nginx.confserver {CORS in Production
testcors.test.jsconst request = require('supertest');Testing CORS Configuration Automatically
cors-origin-examples.jsconst cors = require('cors');Complete Origin Option Reference
cors-options.jsconst corsOptions = {Methods, AllowedHeaders, ExposedHeaders, Credentials, and Ma
dynamic-cors.jsconst cors = require('cors');Dynamic CORS Per-Request via Function
cors-error-handling.jsconst cors = require('cors');CORS Error Handling and Vary Header Behavior
nginx-cors.conflocation /api/ {CORS Behind Reverse Proxies (Nginx, CloudFront)

Key takeaways

1
CORS is a browser-enforced security mechanism
Servers must opt-in via headers; it does not protect against server-to-server attacks.
2
Use the cors package for simplicity, but restrict origins in production
Avoid wildcard origins with credentials; always validate against a whitelist.
3
Preflight requests must be handled correctly
Return 200 with appropriate headers for OPTIONS; place CORS middleware before auth middleware.
4
Test CORS configuration automatically
Include integration tests for allowed/disallowed origins, preflight, and credentials to catch regressions.
5
Origin Option Types
The origin option supports Boolean, String, RegExp, Array, and Function. Use the simplest type that meets your needs; prefer explicit strings or RegExp over arrays for clarity.
6
Preflight Caching with maxAge
Set maxAge to cache preflight responses and reduce OPTIONS requests. A value of 86400 seconds (24 hours) is safe for stable configurations.
7
CORS Behind Reverse Proxies
Offload preflight handling to nginx or CloudFront to reduce load on your Node.js app. Ensure the Origin header is forwarded and the Vary header includes Origin to prevent cache poisoning.
8
Complete Origin Option Reference
The origin option supports Boolean, String, RegExp, Array, and Function types. Use a function for dynamic whitelisting in multi-tenant apps.
9
Methods, AllowedHeaders, ExposedHeaders, Credentials, and MaxAge Options
Configure these to match your API's needs. Use maxAge to cache preflight responses and reduce latency.
10
CORS Error Handling and Vary Header Behavior
Handle CORS errors with a custom middleware to return a 403 JSON response. Always set Vary: Origin when CORS policy depends on the request origin.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is CORS and why is it needed?
Q02SENIOR
How does a preflight request work in CORS?
Q03SENIOR
What is the difference between Access-Control-Allow-Origin: * and a spec...
Q04SENIOR
How do you handle CORS with credentials (cookies) in Express?
Q05SENIOR
What is a common CORS misconfiguration that leads to security vulnerabil...
Q06SENIOR
How would you debug a CORS error in production?
Q01 of 06JUNIOR

What is CORS and why is it needed?

ANSWER
CORS (Cross-Origin Resource Sharing) is a mechanism that allows restricted resources on a web page to be requested from another domain. It's needed because browsers enforce the same-origin policy, which blocks requests from different origins for security reasons. CORS relaxes this by letting servers specify which origins are allowed.
FAQ · 11 QUESTIONS

Frequently Asked Questions

01
What does CORS stand for and why is it needed?
02
How do I enable CORS for all origins in Express?
03
Why is my preflight request failing with a 404?
04
Can I use a wildcard origin with credentials?
05
How do I handle CORS for multiple allowed origins?
06
What is the difference between simple and preflight requests?
07
What is the difference between `allowedHeaders` and `exposedHeaders`?
08
How do I debug CORS issues when the browser shows a generic error?
09
Can I use `maxAge` with credentials?
10
How does `maxAge` affect preflight caching and what is a good value?
11
Why do I get a CORS error even though my server returns the correct headers?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Node.js. Mark it forged?

7 min read · try the examples if you haven't

Previous
Environment Variables in Node.js with dotenv
23 / 47 · Node.js
Next
Input Validation in Node.js with Zod