CORS Error Explained — And How to Fix It on Express.js

Almost every developer hits this wall at some point: your frontend calls your API, works fine in Postman, but the browser console shows a CORS error. This guide explains exactly what is happening, why it exists, and how to fix it correctly — not by blindly pasting a wildcard header and hoping for the best.

The Classic Error

You'll typically see something like this in the browser console:

Access to fetch at 'http://localhost:5000/api/users' 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 not a network failure and not a bug in your fetch call — the request usually reaches the server and even returns a valid response. The browser itself blocks JavaScript from reading that response because the server did not explicitly allow it.

What CORS Actually Is

CORS (Cross-Origin Resource Sharing) is a browser security feature that restricts JavaScript running on one origin (e.g. http://localhost:3000) from reading responses from a different origin (e.g. http://localhost:5000) unless the server explicitly opts in via response headers. Two origins differ if the protocol, domain, or port is different.

  • http://localhost:3000 vs http://localhost:5000 — different ports, different origins
  • http://example.com vs https://example.com — different protocols, different origins
  • https://app.example.com vs https://api.example.com — different subdomains, different origins

This exists to prevent a malicious website from silently making authenticated requests to your bank or email provider using your logged-in browser session and reading the response.

Preflight Requests

For "non-simple" requests — custom headers like Authorization, methods like PUT/DELETE, or a JSON content type — the browser first sends an automatic OPTIONS request to check if the real request is allowed.

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

// Server must respond with something like:
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 OPTIONS correctly — a very common mistake — the browser never sends the real request at all.

Fixing It on Express.js

The easiest and most reliable fix is the official cors npm package, which handles preflight automatically.

npm install cors
const express = require('express');
const cors = require('cors');
const app = express();

app.use(cors({
  origin: ['http://localhost:3000', 'https://myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true, // required if the client sends cookies
}));

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

app.listen(5000);

If you're sending cookies or using fetch(url, { credentials: 'include' }) on the frontend, you cannot use origin: '*' — you must specify the exact allowed origin(s), and set credentials: true on both the server and the fetch call.

Common Mistakes

  • Setting CORS headers only on successful responses, but not on error responses — errors need the header too, or the browser hides the real error from your JS code
  • Forgetting that a reverse proxy (Nginx) in front of your app can strip or duplicate CORS headers if both Nginx and Express try to set them
  • Using origin: '*' together with credentials: true — this combination is invalid and browsers will reject it
  • Not handling the OPTIONS method explicitly when writing CORS headers by hand instead of using the cors package

Frequently Asked Questions

Why does the CORS error happen only in the browser and not in Postman?

CORS is a browser security mechanism, not a server-side restriction. Tools like Postman, curl, or server-to-server requests are not subject to CORS, so they succeed even when the same request would be blocked in a browser tab.

Can I fix a CORS error by setting Access-Control-Allow-Origin to a wildcard?

Setting it to * works for public, unauthenticated APIs, but it cannot be combined with credentials: include or cookies. If your requests use cookies or Authorization headers with credentials, you must return the exact requesting origin instead of a wildcard.

What is a CORS preflight request?

A preflight is an automatic OPTIONS request the browser sends before certain requests, such as those with custom headers or non-simple methods like PUT and DELETE. The server must respond to OPTIONS with the correct CORS headers, or the actual request never gets sent.

Debug Errors Faster with AI

Stuck on a CORS error you can't quite figure out? Paste the exact console message into our free AI Error Explainer for a plain-English cause and fix.

Related articles