How to Fix CORS Errors in Node.js and Express (Complete Guide)
On this page
If you've built a frontend application that calls your Node.js backend, you've probably hit that frustrating CORS error in your browser console. It stops your requests dead and feels like your server is rejecting you for no reason. The truth is simpler: CORS is a security feature, not a bug, and once you understand it, fixing it takes minutes.
TL;DR: Master CORS errors in Express. Learn what causes them, how to fix them, and best practices for production APIs with practical examples.
In this guide, I'll walk you through what CORS actually does, why it fails, and how to configure it properly in Express—from quick fixes to production-ready solutions.
What Is CORS and Why Does It Exist?
CORS stands for Cross-Origin Resource Sharing. It's a browser security mechanism that prevents scripts from one domain from accessing resources on another domain without explicit permission.
Here's the scenario: You have a frontend at https://app.example.com and a backend API at https://api.example.com. When your frontend JavaScript tries to make a request to the backend, the browser checks: "Is the backend allowing requests from this origin?" If not, it blocks the request before it even reaches your server.
This protects users from malicious websites that might try to steal data or perform actions on their behalf. Without CORS, a sketchy website could silently call your bank API using your authentication tokens.
The key thing to understand: CORS errors happen in the browser, not on your server. Your server code executed fine—the browser just refused to show the response to your JavaScript.
Identifying a CORS Error
When CORS fails, you'll see something like this in your browser console:
Access to XMLHttpRequest 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.
The error tells you exactly what's wrong: the response doesn't include the Access-Control-Allow-Origin header, which tells the browser "yes, this origin is allowed."
Other common CORS errors include:
No 'Access-Control-Allow-Credentials' header— when your request includes credentials (cookies, auth headers) but the server doesn't allow themMethod not allowed— the server doesn't allow your HTTP method (POST, PUT, DELETE, etc.)Header not allowed— you're sending a custom header that the server doesn't explicitly allow
The Quick Fix: Using the CORS Package
The fastest way to enable CORS in Express is with the cors npm package. Install it first:
npm install cors
Now enable CORS for all routes:
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.get('/api/data', (req, res) => {
res.json({ message: 'This is now accessible from any origin' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
That's it. The cors() middleware adds the necessary headers to every response. Your browser will now accept requests from any origin.
Warning: This is for development only. In production, you never want to allow requests from everywhere.
Configuring CORS for Specific Origins
In production, you should only allow requests from origins you control. Here's how:
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: 'https://app.example.com',
credentials: true,
optionsSuccessStatus: 200,
};
app.use(cors(corsOptions));
app.get('/api/data', (req, res) => {
res.json({ message: 'Only accessible from https://app.example.com' });
});
app.listen(3000);
The credentials: true option is important if your frontend sends cookies or authorization headers with requests. Without it, the browser will reject authenticated requests even if the origin is allowed.
Allowing Multiple Origins
If you have multiple frontend applications, use a function to dynamically check origins:
const corsOptions = {
origin: function (origin, callback) {
const allowedOrigins = [
'https://app.example.com',
'https://admin.example.com',
'http://localhost:3000', // for development
];
if (allowedOrigins.includes(origin) || !origin) {
callback(null, true);
} else {
callback(new Error('CORS not allowed for this origin'));
}
},
credentials: true,
};
app.use(cors(corsOptions));
The !origin check allows requests with no origin header (like server-to-server requests or curl commands). Remove it if you want to be strict.
Handling Preflight Requests
Before sending certain requests, browsers send a preflight OPTIONS request to check if the server allows the operation. This is automatic—you don't trigger it from your code.
The CORS package handles this automatically, but if you're configuring CORS manually, you need to respond to OPTIONS:
app.options('/api/data', cors());
app.post('/api/data', (req, res) => {
res.json({ message: 'POST successful' });
});
Or, to enable OPTIONS for all routes:
app.options('*', cors());
Enabling CORS for Specific Routes Only
Sometimes you want CORS on some routes but not others. This is useful when you have public APIs and internal ones:
const express = require('express');
const cors = require('cors');
const app = express();
const corsOptions = {
origin: 'https://app.example.com',
credentials: true,
};
// Public routes with CORS
app.get('/api/public/data', cors(), (req, res) => {
res.json({ message: 'Public endpoint' });
});
// Private routes without CORS
app.post('/api/internal/admin', (req, res) => {
res.json({ message: 'Internal endpoint' });
});
// Or use cors on a router:
const publicRouter = express.Router();
publicRouter.use(cors(corsOptions));
publicRouter.get('/users', (req, res) => {
res.json({ users: [] });
});
app.use('/api', publicRouter);
Allowing Specific Headers and Methods
If your frontend sends custom headers or uses less common HTTP methods, you need to explicitly allow them:
const corsOptions = {
origin: 'https://app.example.com',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
credentials: true,
maxAge: 3600, // cache preflight for 1 hour
};
app.use(cors(corsOptions));
The maxAge option tells browsers to cache the preflight response, reducing unnecessary OPTIONS requests and improving performance.
Manual CORS Configuration (No Package)
If you prefer not to use the cors package, you can set headers manually:
app.use((req, res, next) => {
const allowedOrigins = ['https://app.example.com', 'https://admin.example.com'];
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, PATCH'
);
res.setHeader(
'Access-Control-Allow-Headers',
'Content-Type, Authorization'
);
}
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
This gives you more control but is more verbose. Most teams stick with the cors package for simplicity.
Environment-Based Configuration
In development and production, you'll need different CORS settings. Use environment variables:
const corsOptions = {
origin: process.env.CORS_ORIGIN || 'http://localhost:3000',
credentials: true,
maxAge: 3600,
};
app.use(cors(corsOptions));
Then in your .env file:
CORS_ORIGIN=https://app.example.com
For development, you might want to allow multiple origins:
const allowedOrigins = {
development: ['http://localhost:3000', 'http://localhost:3001'],
production: ['https://app.example.com'],
staging: ['https://staging.example.com'],
};
const corsOptions = {
origin: allowedOrigins[process.env.NODE_ENV] || 'http://localhost:3000',
credentials: true,
};
app.use(cors(corsOptions));
Common Pitfalls and How to Avoid Them
Forgetting credentials: true when sending cookies
If your frontend includes credentials with requests, the server must allow them:
// Frontend
fetch('https://api.example.com/data', {
credentials: 'include', // send cookies
});
// Backend
const corsOptions = {
origin: 'https://app.example.com',
credentials: true, // must be true
};
Without credentials: true on the server, the browser rejects the response even if the origin matches.
Wildcard origin with credentials
This won't work:
// ❌ This fails
const corsOptions = {
origin: '*',
credentials: true,
};
When credentials are involved, you can't use a wildcard. You must specify exact origins.
Typos in origin
If you specify http://localhost:3000 but your frontend runs on http://127.0.0.1:3000, they're treated as different origins. Use consistent URLs in development.
Not handling preflight requests
If you manually set CORS headers but don't respond to OPTIONS requests, preflight checks will fail. Always handle OPTIONS or use the cors package.
Testing CORS Locally
To test CORS locally, you can run your frontend and backend on different ports:
Frontend on http://localhost:3000:
fetch('http://localhost:5000/api/data');
Backend on http://localhost:5000 with CORS enabled for http://localhost:3000. This simulates cross-origin requests in development.
Alternatively, use curl to test that headers are present:
curl -i -X OPTIONS http://localhost:5000/api/data \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: GET"
You should see the CORS headers in the response.
Production Checklist
Before deploying, ensure:
- CORS is configured for your actual domain, not a placeholder
- You're not using
origin: '*'if you handle sensitive data or authentication - Credentials are only allowed if needed
- You're setting reasonable
maxAgeto cache preflight responses - Custom headers and methods are explicitly listed
- You're logging CORS rejections for debugging
Summary
CORS errors are nearly always caused by missing or misconfigured response headers. The cors npm package handles this elegantly for most cases. In production, always be explicit about which origins you allow, especially when dealing with authentication or sensitive data.
Start with the specific origin approach in your .env file, and you'll avoid most CORS headaches. The browser's CORS enforcement is annoying while debugging, but it's protecting your users—so respect it.
Sources
Related Articles
How to Debug Node.js Memory Leaks (Step-by-Step Guide)
Learn how to detect, diagnose, and fix Node.js memory leaks using heap snapshots, Chrome DevTools, and clinic.js — with real code examples.
Best Hosting for Next.js Apps in 2026: Vercel vs AWS vs Cloudflare
Compare Vercel, AWS, Cloudflare, and other Next.js hosting platforms. Benchmarks, pricing, and which platform wins for your use case in 2026.
Building an AI Chatbot With LangChain: Practical Developer Guide
Build a production-ready AI chatbot with LangChain, Python, and OpenAI. Step-by-step guide with memory, RAG, streaming, and deployment tips.