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..
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
- ✓Basic Node.js and JavaScript knowledge. Familiarity with npm and Express.js is recommended.
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
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.'
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
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.
Access-Control-Allow-Origin: * with credentials (cookies, auth headers). This is a common misconfiguration that exposes user data.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(, 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())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.
credentials: true, you must specify exact origins in origin. Using * will cause the browser to reject the request.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.
Vary: Origin to prevent CDNs from caching responses for the wrong origin.Vary: Origin header can cause a CDN to serve a cached response with a mismatched Access-Control-Allow-Origin, breaking CORS for some users.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.
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.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.
fetch(url, { credentials: 'include' }) or set xhr.withCredentials = true.SameSite attribute is None and Secure is true for cross-origin requests. Otherwise, modern browsers will block the cookie.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).
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.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.
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.
Origin header without checking it against a whitelist allows any site to make credentialed requests, leading to CSRF-like attacks.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.
Origin header to your backend. Otherwise, your Express app may see no origin and fail to set CORS headers.Access-Control-Allow-Origin. Always set Vary: Origin and ensure the CDN respects it.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.
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.
origin: true unless your API is truly public. Always validate origins against a whitelist to prevent unauthorized access.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.
maxAge to at least 600 seconds to reduce preflight requests. Monitor preflight traffic to tune the value.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.
true or a default origin. Log denied origins for debugging.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.
Vary: Origin if your application server doesn't. Monitor CORS errors via logs to detect misconfigured clients.Vary header includes Origin when the response depends on it.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.
Origin header is forwarded correctly. Test with curl to confirm headers.CORS Misconfiguration Causes Silent Data Leak in Multi-Tenant SaaS
- 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.
| File | Command / Code | Purpose |
|---|---|---|
| server.js | const express = require('express'); | What Is CORS and Why Does It Exist? |
| terminal | curl -H "Origin: https://myfrontend.com" \ | Debugging CORS Issues |
| nginx.conf | server { | CORS in Production |
| test | const request = require('supertest'); | Testing CORS Configuration Automatically |
| cors-origin-examples.js | const cors = require('cors'); | Complete Origin Option Reference |
| cors-options.js | const corsOptions = { | Methods, AllowedHeaders, ExposedHeaders, Credentials, and Ma |
| dynamic-cors.js | const cors = require('cors'); | Dynamic CORS Per-Request via Function |
| cors-error-handling.js | const cors = require('cors'); | CORS Error Handling and Vary Header Behavior |
| nginx-cors.conf | location /api/ { | CORS Behind Reverse Proxies (Nginx, CloudFront) |
Key takeaways
cors package for simplicity, but restrict origins in productionorigin 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.maxAge to cache preflight responses and reduce OPTIONS requests. A value of 86400 seconds (24 hours) is safe for stable configurations.Origin header is forwarded and the Vary header includes Origin to prevent cache poisoning.origin option supports Boolean, String, RegExp, Array, and Function types. Use a function for dynamic whitelisting in multi-tenant apps.maxAge to cache preflight responses and reduce latency.Vary: Origin when CORS policy depends on the request origin.Interview Questions on This Topic
What is CORS and why is it needed?
Frequently Asked Questions
20+ years shipping production JavaScript and front-end systems at scale. Everything here is grounded in real deployments.
That's Node.js. Mark it forged?
7 min read · try the examples if you haven't