How to Fix CORS Error in Express.js: Causes, Solutions & Code Examples
If you have ever built an API with Express.js and tried to call it from a frontend running on a different domain or port, you have almost certainly run into the dreaded CORS error. The browser console flashes a red message like “Access to fetch at … has been blocked by CORS policy” and your request silently fails.
The good news: CORS errors are predictable and fixable once you understand what is happening behind the scenes. In this guide published by the GeminiWeb team, we will walk you through every common cause of CORS issues in Express.js and give you clear, copy-paste code examples for each fix. Whether you are dealing with simple requests, preflight failures, credentials, or multiple allowed origins, this post has you covered.
What Is CORS and Why Does the Error Happen?
CORS stands for Cross-Origin Resource Sharing. It is a security mechanism enforced by web browsers. When your frontend (e.g., https://app.example.com) makes an HTTP request to a backend on a different origin (e.g., https://api.example.com), the browser checks specific HTTP response headers to decide whether the frontend is allowed to read the response.
If those headers are missing or misconfigured on your Express.js server, the browser blocks the response and throws a CORS error. Important: the server still processes the request. CORS is a browser-side enforcement, not a server-side block.
Key CORS Response Headers
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin |
Specifies which origin(s) can access the resource |
Access-Control-Allow-Methods |
Lists the HTTP methods (GET, POST, PUT, DELETE, etc.) allowed |
Access-Control-Allow-Headers |
Lists the custom headers the client is permitted to send |
Access-Control-Allow-Credentials |
Indicates whether cookies/auth headers are allowed |
Access-Control-Max-Age |
How long (in seconds) the browser can cache the preflight response |
Most Common Causes of CORS Errors in Express.js
Before jumping to solutions, let’s identify the root causes. In our experience at GeminiWeb working on dozens of Node.js projects, these are the issues we see most often:
- No CORS headers set at all on the Express server.
- Misconfigured
Access-Control-Allow-Origin(wrong value, missing protocol, or typo). - Preflight (OPTIONS) requests not handled, causing PUT/PATCH/DELETE or custom-header requests to fail.
- Credentials mode enabled on the client but the server uses a wildcard (
*) origin. - Multiple or dynamic origins not supported by the configuration.
- Middleware order issues where CORS headers are set after the route handler runs or after an error is thrown.
- Reverse proxy or hosting platform stripping headers before they reach the browser.
Let’s fix each one.
Solution 1: Use the cors npm Package (Quickest Fix)
The fastest way to fix CORS errors in Express.js is to install the official cors middleware package.
Step-by-step
- Install the package:
npm install cors - Import and use it in your Express app:
const express = require('express'); const cors = require('cors'); const app = express(); // Enable CORS for all origins app.use(cors()); app.get('/api/data', (req, res) => { res.json({ message: 'CORS is working!' }); }); app.listen(3000, () => { console.log('Server running on port 3000'); });
This adds Access-Control-Allow-Origin: * to every response. It is great for development, but not recommended for production because it allows any website to read your API responses.
Solution 2: Allow a Specific Origin
For production, you should restrict access to trusted domains only.
app.use(cors({
origin: 'https://myapp.example.com'
}));
Now only requests originating from https://myapp.example.com will receive the proper CORS headers. Requests from any other origin will be blocked by the browser.
Common mistake: forgetting to include the protocol. myapp.example.com without https:// will not match and the error will persist.
Solution 3: Allow Multiple Origins
If your API serves several frontends (e.g., a marketing site and a dashboard), you need to dynamically set the origin header.
const allowedOrigins = [
'https://myapp.example.com',
'https://dashboard.example.com',
'http://localhost:5173'
];
app.use(cors({
origin: function (origin, callback) {
// Allow requests with no origin (e.g., mobile apps, curl)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
return callback(null, true);
} else {
return callback(new Error('Not allowed by CORS'));
}
}
}));
Pro tip: store your allowed origins in an environment variable so you can change them without redeploying.
# .env
CORS_ORIGINS=https://myapp.example.com,https://dashboard.example.com
const allowedOrigins = process.env.CORS_ORIGINS.split(',');
Solution 4: Handle Preflight Requests Properly
What is a preflight request?
Before sending certain requests (e.g., PUT, PATCH, DELETE, or any request with custom headers like Authorization), the browser sends a preliminary OPTIONS request called a preflight. If your server does not respond to this OPTIONS request with the correct headers, the actual request never fires.
Fix with the cors package
If you are using app.use(cors()) before your routes, preflight is handled automatically. But if you applied CORS only to specific routes, you also need to handle OPTIONS:
// Enable preflight for all routes
app.options('*', cors());
// Then apply cors to your specific route
app.get('/api/data', cors(), (req, res) => {
res.json({ message: 'Hello' });
});
Fix without the cors package (manual headers)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://myapp.example.com');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle preflight
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
Returning a 204 No Content status for OPTIONS tells the browser “go ahead, the actual request is allowed.”
Solution 5: Fix CORS When Using Credentials (Cookies / Auth Headers)
If your frontend sends cookies or an Authorization header, you need two things:
- The client must set
credentials: 'include'(Fetch API) orwithCredentials: true(Axios). - The server must set
Access-Control-Allow-Credentials: trueand must not use a wildcard*for the origin.
Client-side (Axios example)
axios.get('https://api.example.com/user', {
withCredentials: true
});
Server-side
app.use(cors({
origin: 'https://myapp.example.com',
credentials: true
}));
If you use origin: '*' together with credentials: true, the browser will reject the response. This is one of the most frequent mistakes we see.
Solution 6: Expose Custom Response Headers
By default, the browser only exposes a limited set of response headers to JavaScript. If your API returns a custom header (like X-Total-Count for pagination), you need to explicitly expose it:
app.use(cors({
origin: 'https://myapp.example.com',
exposedHeaders: ['X-Total-Count', 'X-Request-Id']
}));
Solution 7: Fix Middleware Order Issues
Express processes middleware in the order it is registered. If your CORS middleware is placed after a route or after an error-handling middleware, the CORS headers may never be set.
Wrong order
app.get('/api/data', (req, res) => {
res.json({ data: 'test' });
});
// CORS middleware registered too late
app.use(cors());
Correct order
// CORS middleware first
app.use(cors());
app.get('/api/data', (req, res) => {
res.json({ data: 'test' });
});
Rule of thumb: always register cors() as one of the very first middleware in your Express app.
Solution 8: CORS Behind a Reverse Proxy (Nginx, Apache, Cloudflare)
Sometimes your Express server sets the correct headers, but a reverse proxy sitting in front of it strips or overrides them. Common scenarios:
- Nginx adds its own
Access-Control-Allow-Originheader, creating a duplicate that the browser rejects. - Cloudflare or a CDN caches the first response (with one origin) and serves it to requests from a different origin.
How to diagnose
- Open browser DevTools and go to the Network tab.
- Click on the failed request and inspect the Response Headers.
- Look for duplicate
Access-Control-Allow-Originheaders or a mismatched origin value.
How to fix
- Set CORS headers in one place only, either in Express or in the proxy, not both.
- If using a CDN, add
Vary: Originto your responses so the CDN caches different versions per origin.
app.use(cors({
origin: 'https://myapp.example.com'
}));
// Add Vary header for CDN compatibility
app.use((req, res, next) => {
res.header('Vary', 'Origin');
next();
});
Complete Production-Ready CORS Configuration
Here is a full example combining all best practices into a single, production-ready setup:
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const app = express();
const allowedOrigins = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',')
: ['http://localhost:5173'];
const corsOptions = {
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Blocked by CORS policy'));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
credentials: true,
maxAge: 86400 // Cache preflight for 24 hours
};
app.use(cors(corsOptions));
// Body parser
app.use(express.json());
// Routes
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS configured correctly!' });
});
// Error handler
app.use((err, req, res, next) => {
if (err.message === 'Blocked by CORS policy') {
return res.status(403).json({ error: 'Origin not allowed' });
}
next(err);
});
app.listen(process.env.PORT || 3000);
Quick Debugging Checklist
If you are still seeing CORS errors after applying fixes, run through this checklist:
| # | Check | Action |
|---|---|---|
| 1 | Is the cors middleware registered before routes? |
Move app.use(cors()) to the top |
| 2 | Does the origin include the protocol? | Use https://example.com not example.com |
| 3 | Are you sending credentials with a wildcard origin? | Replace * with the exact origin |
| 4 | Is a proxy duplicating CORS headers? | Set headers in one layer only |
| 5 | Is the OPTIONS method returning 200 or 204? | Ensure preflight is handled |
| 6 | Are you testing with curl and confused it works? |
Remember: CORS is browser-only |
Frequently Asked Questions
What does CORS stand for?
CORS stands for Cross-Origin Resource Sharing. It is a browser security feature that restricts web pages from making HTTP requests to a different domain than the one that served the page, unless the server explicitly allows it through specific HTTP headers.
Can I use Access-Control-Allow-Origin: * in production?
Technically yes, but it is a security risk. A wildcard origin means any website can make requests to your API and read the responses. If your API handles sensitive data or uses authentication, always specify exact trusted origins instead.
Why does my CORS error only happen with POST or PUT but not GET?
Simple GET requests with standard headers do not trigger a preflight OPTIONS request. However, POST requests with a Content-Type of application/json, or PUT/DELETE requests, do require a preflight. If your server does not handle the OPTIONS method correctly, only these requests will fail.
Do I need both app.use(cors()) and app.options('*', cors())?
No. If you use app.use(cors()) before all your routes, it automatically handles OPTIONS preflight requests as well. You only need app.options() separately if you apply CORS to individual routes instead of globally.
I set CORS headers manually but still get errors. What is wrong?
The most common reasons are: (1) the middleware is registered after the route, (2) the origin value has a typo or missing protocol, (3) a reverse proxy is overwriting or duplicating your headers, or (4) you are sending credentials but using a wildcard origin.
Does CORS protect my API from being called by malicious servers?
No. CORS only applies to browsers. Server-to-server requests, command-line tools like curl, and mobile apps are not restricted by CORS. To protect your API, use authentication, rate limiting, and proper authorization logic.
Wrapping Up
CORS errors in Express.js are frustrating but always solvable. The key is understanding that CORS is a browser security mechanism, not a server error, and that the fix always lives in your server’s response headers.
To recap the essential steps:
- Install and configure the
corsnpm package as early middleware. - Specify exact origins instead of wildcards in production.
- Handle preflight OPTIONS requests.
- Set
credentials: trueon both client and server when using cookies or auth headers. - Avoid duplicate headers when using a reverse proxy.
Need help building or debugging your Express.js API? The team at GeminiWeb specializes in modern web development and can help you ship secure, well-configured backends. Get in touch and let’s solve it together.


