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

Why Does the CORS Error Happen in Node.js Express?

If you have ever built a front-end application that talks to a Node.js Express back-end, you have almost certainly seen this dreaded message in your browser console:

Access to fetch at 'http://localhost:4000/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 error does not come from your Express server. It comes from the browser. And it is doing exactly what it was designed to do: enforcing the same-origin policy.

In this guide we will break down exactly why CORS errors occur, walk through multiple ways to fix them in a Node.js Express API, and cover real-world deployment scenarios so you never get stuck on this problem again.

What Is the Same-Origin Policy?

The same-origin policy is a fundamental browser security mechanism. It prevents JavaScript running on one origin from reading responses that come from a different origin.

Two URLs share the same origin only when all three of the following match:

  • Protocol (http vs https)
  • Host (example.com vs api.example.com)
  • Port (3000 vs 4000)

Quick origin comparison table

Front-end URL Back-end URL Same Origin?
http://localhost:3000 http://localhost:3000/api Yes
http://localhost:3000 http://localhost:4000 No (different port)
https://myapp.com https://api.myapp.com No (different host)
http://myapp.com https://myapp.com No (different protocol)

When the origins differ, the browser sends the request but refuses to let your JavaScript read the response unless the server explicitly opts in through CORS headers.

How CORS Actually Works (The Mechanics)

CORS stands for Cross-Origin Resource Sharing. It is a protocol that uses HTTP headers to tell the browser: “Yes, I allow this origin to access my resources.”

There are two types of CORS requests the browser can make:

1. Simple Requests

A request is considered “simple” when it uses GET, HEAD, or POST with standard content types like application/x-www-form-urlencoded. For these, the browser sends the request directly and checks the response for the Access-Control-Allow-Origin header.

2. Preflight Requests

For anything more complex (PUT, DELETE, PATCH, custom headers, JSON content type), the browser first sends an OPTIONS request called a “preflight.” This preflight asks the server whether the actual request is allowed. If the server does not respond correctly to the OPTIONS request, the browser blocks the real request before it even happens.

Here is what a preflight exchange looks like:

// Browser sends:
OPTIONS /api/users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Content-Type, Authorization

// Server must respond with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

If the server does not handle this OPTIONS request, you get a CORS error.

Solution 1: Using the cors Middleware (Recommended)

The fastest and most reliable way to fix CORS errors in Express is to use the official cors npm package. It handles simple requests, preflight requests, and all the header logic for you.

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 all origins
app.use(cors());

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

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

This sets Access-Control-Allow-Origin: * on every response. It is fine for local development but not recommended for production.

Step 3: Restrict to specific origins (production)

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

app.use(cors(corsOptions));

Dynamic origin with a whitelist

If you need to allow multiple origins dynamically (for example staging and production), you can pass a function:

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

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

app.use(cors(corsOptions));

Solution 2: Setting CORS Headers Manually

If you prefer not to use a third-party package, or if you need very granular control, you can set the headers yourself using Express middleware:

app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, 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: The OPTIONS handler must come before your route definitions. If Express does not respond to the preflight, the browser will block the actual request.

Solution 3: Handling Preflight Requests Explicitly

Sometimes the cors middleware does not catch preflight requests because of middleware ordering issues. In that case you can add an explicit OPTIONS route:

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

// Or for a specific route
app.options('/api/users', cors(corsOptions));

Place this before your other route handlers to make sure Express matches it first.

Common CORS Configuration Mistakes

Even after adding CORS headers, things can still break. Here are the most common mistakes we see:

1. Middleware order is wrong

Express processes middleware in the order you register it. If your routes are defined before the CORS middleware, the headers never get set.

// Wrong order
app.get('/api/data', handler);
app.use(cors()); // Too late!

// Correct order
app.use(cors());
app.get('/api/data', handler);

2. Using credentials with a wildcard origin

If your front-end sends cookies or an Authorization header, you need credentials: true in your CORS config. But the spec does not allow Access-Control-Allow-Origin: * when credentials are involved. You must specify the exact origin.

Setting Wildcard Origin (*) Specific Origin
credentials: false Works Works
credentials: true Fails Works

3. Forgetting to allow the right headers

If your front-end sends a custom header like Authorization and the server does not list it in Access-Control-Allow-Headers, the preflight will fail.

4. A reverse proxy or load balancer strips headers

In production, your CORS headers might be set correctly in Express but get stripped by Nginx, Apache, or a cloud load balancer before reaching the browser. Always verify the final response headers using your browser DevTools Network tab.

Fixing CORS for Specific Deployment Scenarios

Deploying to Vercel

When you deploy a Node.js Express API to Vercel, you may need to add CORS headers in your vercel.json in addition to your Express middleware:

{
  "headers": [
    {
      "source": "/api/(.*)",
      "headers": [
        { "key": "Access-Control-Allow-Origin", "value": "https://myapp.com" },
        { "key": "Access-Control-Allow-Methods", "value": "GET,POST,PUT,DELETE,OPTIONS" },
        { "key": "Access-Control-Allow-Headers", "value": "Content-Type, Authorization" }
      ]
    }
  ]
}

Deploying behind Nginx as a reverse proxy

If Nginx sits in front of your Express app, you can either let Express handle CORS (make sure Nginx passes all headers through) or set the headers at the Nginx level:

location /api/ {
    proxy_pass http://localhost:4000;
    add_header Access-Control-Allow-Origin https://myapp.com always;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;

    if ($request_method = OPTIONS) {
        return 204;
    }
}

Tip: Do not set CORS headers in both Nginx and Express. Duplicate headers can cause unexpected browser behavior.

Using a front-end dev proxy (React, Vue, Angular)

During local development, many front-end frameworks let you proxy API requests so the browser thinks everything comes from the same origin. For example in a React project using Vite, add this to vite.config.js:

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true
      }
    }
  }
});

This avoids CORS entirely in development because the browser only sees one origin. You will still need proper CORS configuration for your production deployment.

Full Working Example

Here is a complete Express server with production-ready CORS configuration:

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

const app = express();

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

// Add localhost in development
if (process.env.NODE_ENV !== 'production') {
  allowedOrigins.push('http://localhost:3000');
  allowedOrigins.push('http://localhost:5173');
}

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

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

// Apply CORS to all routes
app.use(cors(corsOptions));

app.use(express.json());

app.get('/api/data', (req, res) => {
  res.json({ message: 'Hello from the API' });
});

app.listen(4000, () => {
  console.log('API server running on port 4000');
});

Debugging Checklist

If you are still seeing CORS errors after applying a fix, run through this checklist:

  1. Open the Network tab in your browser DevTools.
  2. Find the failing request and check whether an OPTIONS preflight was sent.
  3. Inspect the response headers of the preflight. Is Access-Control-Allow-Origin present?
  4. Does the allowed origin exactly match your front-end URL (including protocol and port)?
  5. If using credentials, make sure the origin is not *.
  6. Confirm your CORS middleware runs before your route handlers.
  7. Check if a reverse proxy, CDN, or hosting platform is adding or stripping headers.
  8. Clear your browser cache. Old preflight responses can be cached.

Frequently Asked Questions

What does CORS stand for?

CORS stands for Cross-Origin Resource Sharing. It is a mechanism that uses HTTP headers to grant a web application running at one origin permission to access resources from a different origin.

Is the CORS error a server error or a browser error?

It is enforced by the browser. The server may respond successfully (HTTP 200), but the browser will block your JavaScript from reading that response if the correct CORS headers are missing.

Should I use Access-Control-Allow-Origin: * in production?

Generally no. A wildcard origin allows any website to call your API. This is fine for truly public APIs, but for most applications you should specify the exact origins you trust.

Why does my GET request work but my POST request fails?

If your POST request sends a Content-Type: application/json header, the browser classifies it as a non-simple request and sends a preflight OPTIONS request first. If your server does not handle OPTIONS, the request is blocked.

Can I fix CORS from the front-end?

No. CORS is a server-side configuration. The only front-end workaround during development is using a proxy (as shown above), which avoids cross-origin requests entirely by routing everything through the same host.

Do mobile apps and Postman have CORS errors?

No. CORS is a browser-only security feature. Tools like Postman, curl, and native mobile apps do not enforce the same-origin policy, which is why your API calls work there but fail in the browser.

How do I allow multiple origins without using a wildcard?

Use the dynamic origin function approach shown above. The cors middleware accepts a function that checks the incoming origin against a whitelist and returns the matching origin in the response header.

Leave a Comment