How to Fix CORS Error in Express.js API: Causes and Solutions

by | May 7, 2026 | Uncategorized | 0 comments

Why Does the CORS Error Happen in Express.js?

If you have ever tried to call your Express.js backend from a frontend application running on a different origin, you have almost certainly seen this error in your browser console:

Access to fetch at 'http://localhost:5000/api/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This is the browser enforcing the Same-Origin Policy. In simple terms, a web page served from http://localhost:3000 is not allowed to make HTTP requests to http://localhost:5000 unless the server at port 5000 explicitly says it is okay. The mechanism that handles this permission is called CORS (Cross-Origin Resource Sharing).

Your Express.js server does not send CORS headers by default. That is why the browser blocks the response. The fix is to tell Express to include the correct headers. This post walks you through every common scenario, from the quickest one-liner to advanced configurations for production deployments.

Quick Glossary: Terms You Need to Know

Term Meaning
Origin The combination of protocol, domain, and port (e.g. https://example.com:443)
Preflight request An OPTIONS request the browser sends before certain cross-origin requests to check permissions
Access-Control-Allow-Origin The response header that tells the browser which origins are allowed
Simple request A GET, HEAD, or POST request with standard headers that does not trigger a preflight
cors middleware The popular npm package (cors) that automates CORS header management in Express

Most Common Causes of CORS Errors in Express.js

Before jumping to solutions, it helps to identify why you are seeing the error. Here are the scenarios we encounter most often:

  1. No CORS configuration at all – Express does not add any Access-Control-* headers by default.
  2. Mismatched origins – Your allowed origin is http://localhost:3000 but the frontend runs on http://127.0.0.1:3000. These are different origins.
  3. Trailing slash in the origin – Setting the allowed origin to http://localhost:3000/ (with a trailing slash) will silently fail. Origins never include a path.
  4. Preflight not handled – You set headers for regular requests but forgot to respond to the OPTIONS method.
  5. Middleware order is wrong – CORS middleware is placed after the route handler, so headers are never set before the response is sent.
  6. Credentials without explicit origin – Using credentials: 'include' on the frontend while the server uses Access-Control-Allow-Origin: *. Browsers reject this combination.
  7. Reverse proxy stripping headers – Nginx, Cloudflare, or another proxy removes or overwrites your CORS headers.

Solution 1: Using the cors npm Package (Recommended)

The fastest and most reliable way to fix CORS errors in Express.js is to use the official cors middleware.

Step 1: Install the package

npm install cors

Step 2: Enable CORS for all origins (development)

const express = require('express');
const cors = require('cors');

const app = express();

// Allow every origin - suitable for local development only
app.use(cors());

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

app.listen(5000, () => console.log('Server running on port 5000'));

This adds the header Access-Control-Allow-Origin: * to every response. It also handles preflight OPTIONS requests automatically.

Step 3: Restrict to specific origins (production)

Using a wildcard in production is a security risk. Instead, pass an options object:

const corsOptions = {
  origin: 'https://myapp.example.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true
};

app.use(cors(corsOptions));

Allow multiple origins

If you need to allow more than one domain, pass an array or a function:

const allowedOrigins = [
  'https://myapp.example.com',
  'https://admin.example.com'
];

const corsOptions = {
  origin: function (origin, callback) {
    // Allow requests with no origin (mobile apps, curl, etc.)
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true
};

app.use(cors(corsOptions));

Use environment variables for flexibility

const corsOptions = {
  origin: process.env.ALLOWED_ORIGIN || 'http://localhost:3000',
  credentials: true
};

app.use(cors(corsOptions));

This lets you change the allowed origin per environment without modifying code.

Solution 2: Setting CORS Headers Manually

If you prefer not to add a dependency, you can write your own middleware:

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://myapp.example.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');

  // Handle preflight
  if (req.method === 'OPTIONS') {
    return res.sendStatus(204);
  }

  next();
});

Important: This middleware must be placed before your route definitions. If it comes after, Express will have already sent the response without the headers.

Solution 3: Handling Preflight (OPTIONS) Requests Properly

Preflight requests are triggered when the browser detects a “non-simple” request. Common triggers include:

  • Using methods other than GET, HEAD, or POST
  • Sending custom headers like Authorization or X-Custom-Header
  • Using Content-Type: application/json with POST

The browser sends an OPTIONS request first. If your server does not respond correctly to it, the actual request never happens.

Symptoms of a preflight issue

  • GET requests work but POST or PUT requests fail
  • You see two requests in the Network tab: an OPTIONS followed by a failed request
  • The error mentions “preflight” or “OPTIONS”

Fix with the cors package

The cors middleware handles this automatically when used with app.use(cors()). If you apply CORS only to specific routes, add a global preflight handler:

// Enable preflight for all routes
app.options('*', cors(corsOptions));

// Then apply cors to specific routes
app.get('/api/data', cors(corsOptions), (req, res) => {
  res.json({ result: 'ok' });
});

Fix manually

app.options('*', (req, res) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://myapp.example.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-Max-Age', '86400'); // Cache preflight for 24 hours
  res.sendStatus(204);
});

Common Misconfigurations and How to Fix Them

Misconfiguration What Happens Fix
Trailing slash in origin (http://localhost:3000/) Origin never matches, CORS header not sent Remove the trailing slash
credentials: true with origin: '*' Browser rejects the response Set an explicit origin instead of wildcard
CORS middleware placed after routes Headers are never added Move app.use(cors()) above all route definitions
Using http in origin but frontend is on https Origin mismatch Match the exact protocol the frontend uses
Nginx/proxy overwriting CORS headers Express sets headers but they get stripped Configure the proxy to pass through CORS headers, or handle CORS at the proxy level only
Error handler sending responses without CORS headers Successful requests work, but error responses trigger CORS errors in the browser Place CORS middleware before error handlers too

CORS Configuration for Different Deployment Setups

Express behind Nginx reverse proxy

When Nginx sits in front of Express, you have two options:

  1. Handle CORS in Express and configure Nginx to pass headers through without modification.
  2. Handle CORS in Nginx only and skip the cors middleware entirely.

Do not set CORS headers in both places. Duplicate Access-Control-Allow-Origin headers will cause the browser to reject the response.

If you handle CORS in Nginx:

location /api/ {
    if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' 'https://myapp.example.com';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
        add_header 'Access-Control-Max-Age' 86400;
        return 204;
    }
    add_header 'Access-Control-Allow-Origin' 'https://myapp.example.com' always;
    proxy_pass http://localhost:5000;
}

Express deployed on a cloud platform (Render, Railway, Fly.io, etc.)

Most cloud platforms proxy traffic through their own infrastructure. The recommendation is to handle CORS inside your Express app using the cors package. Make sure to set the origin option to your frontend’s actual production URL.

Express with a serverless adapter (AWS Lambda, Vercel Functions)

Serverless environments sometimes strip or cache headers differently. Always return CORS headers from both the preflight handler and the main handler. Using the cors middleware works in most serverless Express adapters, but verify by inspecting response headers in your browser’s Network tab.

Debugging CORS Errors Step by Step

If you have applied a fix but the error persists, follow this checklist:

  1. Open the Network tab in your browser DevTools.
  2. Find the failing request. Check if there is an OPTIONS request before it.
  3. Inspect the response headers of the OPTIONS request. Look for Access-Control-Allow-Origin. If it is missing, your server is not handling the preflight.
  4. Compare the Origin request header with the Access-Control-Allow-Origin response header. They must match exactly (protocol, domain, and port).
  5. Check for duplicate headers. Two Access-Control-Allow-Origin headers will cause a failure.
  6. Test with curl to rule out browser caching:
    curl -I -X OPTIONS -H "Origin: http://localhost:3000" http://localhost:5000/api/data
  7. Clear browser cache or test in incognito mode. Preflight responses can be cached aggressively.

Complete Working Example

Here is a production-ready Express.js setup with proper CORS handling:

const express = require('express');
const cors = require('cors');
require('dotenv').config();

const app = express();

// ---- CORS Configuration ----
const allowedOrigins = process.env.ALLOWED_ORIGINS
  ? process.env.ALLOWED_ORIGINS.split(',')
  : ['http://localhost:3000'];

const corsOptions = {
  origin: function (origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Blocked by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
};

// Apply CORS before any routes
app.use(cors(corsOptions));

// Body parser
app.use(express.json());

// Routes
app.get('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

app.post('/api/data', (req, res) => {
  res.json({ received: req.body });
});

// Error handler (CORS headers are already set by middleware above)
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Internal server error' });
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server listening on port ${PORT}`));

In your .env file:

ALLOWED_ORIGINS=https://myapp.example.com,https://admin.example.com

Security Best Practices for CORS in Express.js

  • Never use origin: '*' in production. Always whitelist specific domains.
  • Limit allowed methods to only the ones your API actually uses.
  • Limit allowed headers to the ones your frontend sends.
  • Set maxAge to reduce the number of preflight requests the browser makes.
  • Use credentials: true only if needed (e.g., when sending cookies or HTTP authentication).
  • Validate origins dynamically using environment variables so you do not hard-code domains in your source code.
  • Handle CORS in one place only to avoid conflicting or duplicate headers from your proxy and your app.

Frequently Asked Questions

What does CORS stand for?

CORS stands for Cross-Origin Resource Sharing. It is a security feature built into web browsers that restricts web pages from making requests to a different origin than the one that served the page.

Why do I only see CORS errors in the browser and not in Postman or curl?

CORS is enforced by web browsers only. Tools like Postman, curl, and server-side HTTP clients do not implement the Same-Origin Policy, so they never block cross-origin responses.

Can I fix CORS errors from the frontend?

No. CORS is controlled entirely by the server. The server must include the correct Access-Control-Allow-Origin header in its responses. The only frontend workaround is to route requests through a same-origin proxy (e.g., a Next.js API route or a Vite dev server proxy), which avoids cross-origin requests altogether.

Does app.use(cors()) handle preflight requests automatically?

Yes. When you use app.use(cors()) at the application level, the middleware intercepts OPTIONS requests and responds with the appropriate headers. You do not need to add a separate app.options() handler unless you are applying CORS to individual routes only.

Why do I get a CORS error even after installing the cors package?

The most common reasons are:

  • The middleware is placed after your route definitions.
  • There is a trailing slash in your origin configuration.
  • The protocol or port in the origin does not match exactly.
  • A reverse proxy is stripping or duplicating the headers.

Use the debugging checklist earlier in this article to identify the exact cause.

Should I handle CORS in Nginx or in Express?

Pick one, not both. If your Express app is the only backend behind Nginx, handling CORS in Express gives you more flexibility (dynamic origins, per-route configuration). If you have multiple backend services behind Nginx, handling CORS at the Nginx level keeps the configuration centralized.

Is there a performance cost to CORS?

The main cost is the extra preflight OPTIONS request. You can minimize this by setting the Access-Control-Max-Age header, which tells the browser to cache the preflight result. A value of 86400 (24 hours) is common and reduces unnecessary round trips significantly.

Search

Recent Posts

Subscribe