Meta Description: I’ve debugged dozens of CORS errors in Node.js APIs. Here’s exactly how to diagnose and fix them properly — without disabling security.
“Access to fetch at ‘http://localhost:5000/api/users’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.”
If you’ve built a Node.js API with a separate frontend, you’ve seen this message. I’ve seen it more times than I can count, and I’ve also seen just as many developers “fix” it by slapping Access-Control-Allow-Origin: * on everything and calling it done — which works, until it doesn’t, and often introduces a real security gap.
In my experience, most CORS errors aren’t actually about CORS being “broken.” They’re about not understanding what the browser is enforcing and why. Once that clicks, the fix is usually five minutes of work. Let me walk through exactly how I diagnose and resolve these in production Node.js APIs.
Quick Answer: How to Fix a CORS Error in Node.js
- Identify the exact origin the browser is blocking (check the browser console error message carefully).
- Install and configure the
corsnpm package in your Express (or other framework) API. - Explicitly whitelist your frontend’s origin instead of using
*, especially if you’re sending cookies or auth headers. - Handle preflight
OPTIONSrequests correctly — this is where most manual CORS implementations fail. - Set
credentials: trueon both the server and the client if you’re using cookies orAuthorizationheaders with credentialed requests. - Double-check your reverse proxy or API Gateway isn’t stripping CORS headers before they reach the browser.
Let’s go deeper into each of these, because the details matter more than they seem to at first.
What CORS Actually Is (And Why It’s Not a Bug)
CORS (Cross-Origin Resource Sharing) is a browser security mechanism, not a server-side restriction and not something your Node.js code is inherently missing. By default, browsers block JavaScript from making requests to a different origin (different domain, protocol, or port) than the page that’s running the script — this is called the Same-Origin Policy.
Fundamento: CORS errors happen in the browser, not on your server. Your API almost always receives and processes the request just fine — the browser simply refuses to hand the response back to your JavaScript code unless the server explicitly says it’s allowed via response headers.
This is why testing your API in Postman or curl “works fine” while the exact same request fails in the browser — those tools don’t enforce CORS at all, because CORS is a browser policy, not an HTTP requirement.
Step 1: Read the Actual Error Message
I always start here, because the error message tells you exactly what’s missing. A few common variants I see constantly:
Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present
on the requested resource.
→ Your server isn’t sending the header at all.
...has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header has a
value 'http://localhost:3000' that is not equal to the supplied origin.
→ You’ve hardcoded the wrong origin, or your app is running on a different port than expected.
...has been blocked by CORS policy: Response to preflight request doesn't pass access
control check: It does not have HTTP ok status.
→ Your server isn’t handling the OPTIONS preflight request correctly — very common with custom auth middleware that runs before CORS headers are set.
Step 2: Install and Configure the cors Package
I almost never hand-roll CORS headers manually anymore — the cors npm package handles the edge cases (preflight, multiple origins, credentials) correctly out of the box.
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
// Basic setup — fine for public APIs with no cookies/credentials
app.use(cors({
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
app.listen(5000);
Why I explicitly set origin instead of using '*': wildcard origins can’t be combined with credentialed requests (cookies, Authorization headers) per the CORS spec — browsers will reject it. Even for public APIs, I prefer being explicit because it documents intent and avoids surprises later when the API grows.
Step 3: Supporting Multiple Origins (Dev, Staging, Production)
This is the setup I use on almost every real project, since you’re usually dealing with localhost:3000 in development and a real domain in production.
const allowedOrigins = [
'http://localhost:3000',
'https://staging.example.com',
'https://app.example.com',
];
app.use(cors({
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,
}));
Ponto de Atenção: Requests with no
Originheader (server-to-server calls, curl, mobile apps, some Postman configurations) will pass the!origincheck above. That’s expected — CORS only governs browser behavior, so this isn’t a security hole in itself, but don’t rely on origin-checking as your only access control if the endpoint needs real authentication.
Step 4: Handling Credentials (Cookies and Auth Headers) Correctly
This is where I see the most confusion. If your frontend needs to send cookies or an Authorization header with a cross-origin request, you need changes on both sides:
Server (Node.js/Express):
app.use(cors({
origin: 'https://app.example.com', // must be a specific origin, NOT '*'
credentials: true,
}));
Client (fetch):
fetch('https://api.example.com/api/users', {
method: 'GET',
credentials: 'include', // tells the browser to send cookies cross-origin
});
Client (axios):
axios.get('https://api.example.com/api/users', {
withCredentials: true,
});
I learned this the hard way on an early project: I had credentials: true on the server but forgot credentials: 'include' on the fetch call. The cookie-based session simply never showed up server-side, and it took me an embarrassingly long time to realize the fix wasn’t on the backend at all.
Step 5: Fixing Preflight (OPTIONS) Request Failures
For “non-simple” requests — anything using PUT, DELETE, PATCH, custom headers, or Content-Type: application/json — the browser first sends an automatic OPTIONS preflight request to check if the actual request is allowed. If your server doesn’t respond to OPTIONS correctly, the real request never even fires.
Common cause I see in code reviews: authentication middleware running before the CORS middleware, rejecting the OPTIONS request (which has no auth token) before CORS headers ever get set.
// WRONG ORDER — auth middleware blocks the preflight request
app.use(authMiddleware);
app.use(cors());
// CORRECT ORDER — CORS runs first, preflight requests succeed
app.use(cors());
app.use(authMiddleware);
With the cors package, app.use(cors()) automatically handles OPTIONS requests for you across all routes. If you’re not using the package and handling it manually, you need an explicit route:
app.options('*', (req, res) => {
res.header('Access-Control-Allow-Origin', 'https://app.example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.sendStatus(204);
});
Step 6: Check Your Reverse Proxy or API Gateway
If your Node.js app sits behind Nginx, an AWS API Gateway, or a load balancer, CORS headers set correctly by Express can still get stripped before reaching the browser. I always verify with a raw request:
curl -i -X OPTIONS https://api.example.com/api/users \
-H "Origin: https://app.example.com" \
-H "Access-Control-Request-Method: POST"
If Access-Control-Allow-Origin doesn’t show up in the response headers here, the problem is happening at the proxy layer, not in your Node.js code — and no amount of changing your Express config will fix it. For Nginx, you typically need explicit add_header directives; for API Gateway, CORS needs to be configured at the gateway level in addition to (or instead of) your Lambda/Node code.
CORS Configuration Comparison
| Approach | Security | Complexity | When I Use It |
|---|---|---|---|
origin: '*' | Low — no credential support, open to any site | Very low | Public, read-only APIs with no sensitive data |
| Static origin string | Good | Low | Single-frontend apps with one known domain |
| Origin allowlist function | Good | Medium | Multi-environment apps (dev/staging/prod) |
| Dynamic origin from database/config | Good | Higher | Multi-tenant SaaS platforms with customer-specific domains |
| Reverse proxy-level CORS | Good | Medium-High | When the Node.js app itself shouldn’t need to know about origins |
Common CORS Mistakes I See Constantly
- Using
'*'together withcredentials: true. This combination is invalid per spec and browsers will reject it outright. - Setting CORS headers only on success responses, forgetting error-handling middleware also needs to include them — otherwise a 500 error looks like a CORS error in the browser console, which sends people debugging the wrong thing entirely.
- Auth middleware running before CORS middleware, blocking preflight requests as described above.
- Forgetting
credentials: 'include'on the client while enabling it on the server, or vice versa. - Testing exclusively with Postman/curl and assuming the API is “fine” because those tools don’t enforce CORS at all.
- Not accounting for
wwwvs non-wwworhttpvshttpsas genuinely different origins —https://example.comandhttps://www.example.comare not the same origin.
Troubleshooting Checklist
- [ ] Confirmed the exact error message and which header/behavior is missing
- [ ] Verified the request works in Postman/curl (expected) but fails in browser (also expected — this doesn’t mean the API is broken)
- [ ]
corsmiddleware installed and placed before other route-blocking middleware - [ ] Origin explicitly whitelisted (not
'*'if credentials are involved) - [ ]
credentials: trueset server-side andcredentials: 'include'/withCredentials: trueset client-side, if using cookies/auth headers - [ ] Preflight
OPTIONSrequests return a 2xx status with correct headers - [ ] Verified headers survive any reverse proxy/API Gateway layer with a raw
curlrequest - [ ] Error-handling middleware also sends CORS headers, not just success paths
Frequently Asked Questions
Why do I get a CORS error even though my API works fine in Postman? Postman and curl don’t enforce CORS — it’s a browser-only security mechanism. Your server logic is likely working correctly; the browser is simply blocking your frontend JavaScript from reading the response because the required Access-Control-Allow-Origin header isn’t present or doesn’t match.
How do I allow multiple origins in a Node.js CORS setup? Use a function for the origin option in the cors package that checks the incoming request’s origin against an allowlist array, and calls back with true or false accordingly, as shown in Step 3 above.
Why isn’t Access-Control-Allow-Origin: '*' working with my cookies? Per the CORS specification, wildcard origins cannot be used alongside credentialed requests (cookies, Authorization headers). You must specify an exact origin string when credentials: true is set.
Do I need to handle OPTIONS requests manually in Express? Not if you’re using the cors npm package — it automatically handles preflight OPTIONS requests for all routes. If you’re setting headers manually without the package, you need an explicit app.options() handler.
Can CORS be fixed entirely on the frontend? No. CORS is enforced by the browser based on headers the server sends. No frontend code, fetch configuration, or React setting can bypass it — the fix has to happen on the server (or proxy) that’s being called.
Why does my CORS error only happen in production, not locally? This is usually because your production frontend runs on a different domain than the one whitelisted in your server’s CORS config, or because a reverse proxy/CDN in production is stripping CORS headers that your local Express server sends directly.
Is disabling CORS with a browser extension a real fix? No — that only disables the check in your own browser for testing purposes. Every other user’s browser will still enforce CORS normally, so it doesn’t fix anything for actual users.
Final Thoughts
Once you understand that CORS is the browser protecting the user, not your server malfunctioning, debugging it stops being frustrating and starts being mechanical: read the exact error, check the response headers, verify preflight handling, and confirm nothing upstream is stripping headers. In my experience, that sequence resolves the overwhelming majority of CORS issues in Node.js APIs without ever needing to compromise on security with a blanket '*' origin.
If you’re working through backend or API issues like this one, take a look at the other Node.js and backend development guides here on SpiritCode.blog — they’re all written from real debugging sessions, not just documentation summaries.
