Engineering7 min read

Fix Express CORS Error: "No 'Access-Control-Allow-Origin' Header" (2026 Guide)

Express CORS errors block your frontend from talking to your API. Here are the exact causes and fixes — from missing headers to preflight failures to credentials.

ExpressNode.jsCORSAPIdebugging

What Is a CORS Error?

CORS (Cross-Origin Resource Sharing) is a browser security policy. When your frontend (e.g., localhost:3000) tries to fetch from your API (e.g., localhost:3001), the browser checks whether the server explicitly allows that origin.

Error: Access to fetch at 'http://localhost:3001/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.

CORS errors only happen in the browser. curl and Postman bypass CORS entirely — if your API works in Postman but fails in the browser, CORS is the problem.

The Fastest Fix (And Why to Read Further)

bash
npm install cors

const cors = require('cors')
app.use(cors())

This allows ALL origins. Works immediately. Don't ship this to production. It opens your API to any website. Read the sections below to configure
it correctly.

Correct CORS Setup for Express

const cors = require('cors')

const corsOptions = {
  origin: process.env.FRONTEND_URL || 'http://localhost:3000',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}

app.use(cors(corsOptions))

▎ Note: Set origin to your actual frontend URL — not '*'. If you need multiple origins, pass an array: origin: ['https://app.example.com',
▎ 'https://staging.example.com'].

Cause 1: CORS Middleware Added After Routes

Express middleware runs in order. If you define routes before app.use(cors()), requests to those routes skip CORS entirely.

// ❌ Routes before cors() — CORS headers never added
app.get('/api/users', (req, res) => res.json(users))
app.use(cors())

▎ Fix: Always put app.use(cors()) before any route definitions.

// ✅ cors() runs first on every request
const app = express()
app.use(cors(corsOptions))
app.use(express.json())
app.get('/api/users', (req, res) => res.json(users))

Cause 2: Preflight OPTIONS Request Not Handled

Browsers send a preflight OPTIONS request before any non-simple request (PUT, DELETE, requests with Authorization header). If Express doesn't
handle OPTIONS, the actual request never fires.

▎ Error: CORS preflight channel did not succeed

▎ Fix: Explicitly handle OPTIONS on all routes.

// ✅ Handle preflight on all routes
app.options('*', cors(corsOptions))
app.use(cors(corsOptions))

Cause 3: Credentials Not Configured

Sending cookies or Authorization headers requires credentials: true on the server AND credentials: 'include' on the client. Missing either one
blocks the request.

// ❌ Credentials not enabled
app.use(cors({ origin: 'http://localhost:3000' }))

▎ Fix: Enable credentials on both sides. Wildcard origin '*' is rejected by browsers when credentials: true.

// ✅ Server
app.use(cors({
  origin: 'http://localhost:3000',
  credentials: true,
}))

// ✅ Client
fetch('http://localhost:3001/api/me', {
  credentials: 'include',
  headers: { 'Authorization': `Bearer ${token}` },
})

Cause 4: Wrong Origin in CORS Config

Origin must match exactly — trailing slash, wrong port, or wrong protocol = blocked.

// ❌ Config has port 3001, browser sends port 3000
app.use(cors({ origin: 'http://localhost:3001' }))

▎ Fix: Open DevTools → Network → click blocked request → Request Headers → Origin. Copy that value exactly into your config.

Cause 5: Works Locally but Fails in Production

Config has localhost:3000 hardcoded, but production frontend is https://app.example.com.

▎ Fix: Use environment variables for origin.

// ✅ env var per environment
const allowedOrigins = (process.env.ALLOWED_ORIGINS || 'http://localhost:3000').split(',')

app.use(cors({
  origin: (origin, callback) => {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true)
    } else {
      callback(new Error(`CORS blocked: ${origin}`))
    }
  },
  credentials: true,
}))

Set ALLOWED_ORIGINS=https://app.example.com,https://staging.example.com in production env.

▎ Warning: Don't use origin: '*' with credentials: true. Browsers block this combination. Use a function or array for origin instead.

Debug CORS in 60 Seconds

1. Open DevTools → Network
2. Click the failing request
3. Check Response Headers for Access-Control-Allow-Origin
4. Missing header → cors middleware not running (check order)
5. Wrong value → origin mismatch
6. Preflight failing → add app.options('*', cors(corsOptions))

Quick Reference

┌───────────────────────────────────────┬────────────────────────────────────────┬────────────────────────────────────────────┐
│                Symptom                │                 Cause                  │                    Fix                     │
├───────────────────────────────────────┼────────────────────────────────────────┼────────────────────────────────────────────┤
│ No Access-Control-Allow-Origin header │ cors() not applied to route            │ Move app.use(cors()) before routes         │
├───────────────────────────────────────┼────────────────────────────────────────┼────────────────────────────────────────────┤
│ Preflight fails                       │ OPTIONS not handled                    │ Add app.options('*', cors())               │
├───────────────────────────────────────┼────────────────────────────────────────┼────────────────────────────────────────────┤
│ Credentials blocked                   │ credentials: true missing              │ Add to both server config and client fetch │
├───────────────────────────────────────┼────────────────────────────────────────┼────────────────────────────────────────────┤
│ Works locally, fails in production    │ Hardcoded localhost origin             │ Use env var for origin                     │
├───────────────────────────────────────┼────────────────────────────────────────┼────────────────────────────────────────────┤
│ * origin with credentials             │ Wildcard + credentials = browser block │ Use explicit origin with credentials       │
└───────────────────────────────────────┴────────────────────────────────────────┴────────────────────────────────────────────┘

Debug faster starting today.

Free VS Code extension. 10 sessions/day. No credit card.

Install Free →

Related Posts

Engineering

Codebase-Aware AI Debugging vs Generic AI: Why Context Changes Everything

7 min read

Engineering

How to Debug a React Application in VS Code (Complete Guide)

8 min read

← All posts