On this page

Engineering6 min read

Fix Express CORS Error: No 'Access-Control-Allow-Origin' Header

Express CORS errors are blocked by the browser, not caused by a server crash. Here's why they happen, how to configure the cors package correctly for production, handle preflight requests, send credentials, and debug CORS in 60 seconds.

expresscorsnodejsjavascriptapi

What's Actually Happening

The browser blocked the response, not the server. Your Express API received the request and responded fine. The browser checked the response headers, did not find Access-Control-Allow-Origin, and blocked your JavaScript from reading the response.

Two things follow from this:

  • curl and Postman do not enforce CORS. If your API works there but fails in the browser, CORS is the problem.
  • Adding the right response headers to Express is the entire fix.

The Quickest Fix

bash
npm install cors
javascript
const express = require('express')
const cors = require('cors')

const app = express()
app.use(cors())

cors() with no arguments allows every origin. It unblocks you in development but do not ship this to production. Any website can hit your API.

Production Setup

Lock it to your actual frontend URL:

javascript
const corsOptions = {
  origin: process.env.ALLOWED_ORIGIN || 'https://yourdomain.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}

app.use(cors(corsOptions))

Set ALLOWED_ORIGIN in your production environment variables. No code changes needed when you deploy to a new domain.

Warning: origin: '*' and credentials: true together are rejected by browsers. If you are sending cookies or auth headers, you must specify the exact origin.

Multiple Origins

javascript
const allowedOrigins = [
  'https://yourdomain.com',
  'https://staging.yourdomain.com',
  'http://localhost:3000',
]

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

Preflight Requests

Browsers send a preflight OPTIONS request before any POST, PUT, DELETE, or request with custom headers like Authorization. If Express does not respond to OPTIONS correctly, the real request never fires.

The cors package handles this automatically, but only if it runs before your routes:

javascript
// cors before routes
app.use(cors(corsOptions))
app.use(express.json())
app.use('/api', router)

// cors after routes — preflight fails
app.use('/api', router)
app.use(cors(corsOptions))

If you are still seeing preflight failures, add an explicit handler:

javascript
app.options('*', cors(corsOptions))
app.use(cors(corsOptions))

Credentials and Auth Headers

If your frontend sends cookies or an Authorization header:

javascript
// Frontend
fetch('https://api.yourdomain.com/users', {
  credentials: 'include',
  headers: { 'Authorization': `Bearer ${token}` },
})

Your Express server needs to match:

javascript
app.use(cors({
  origin: 'https://yourdomain.com',
  credentials: true,
}))

Note: The error Access-Control-Allow-Origin must not be '*' when credentials mode is 'include' means you have origin: '*' and credentials: true together. Replace '*' with your exact frontend URL.

Without the cors Package

javascript
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'https://yourdomain.com')
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
  res.header('Access-Control-Allow-Credentials', 'true')

  if (req.method === 'OPTIONS') return res.sendStatus(200)
  next()
})

Diagnosing the Exact Cause

bash
# Test without CORS enforcement
curl -I http://localhost:3001/api/users

# Simulate a preflight request
curl -X OPTIONS http://localhost:3001/api/users \
  -H "Origin: http://localhost:3000" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  -v

Look for Access-Control-Allow-Origin in the response. If it is missing, cors() is not running on that route. If it is * and you are sending credentials, that is the mismatch.

Quick Reference

SymptomCauseFix
Works in Postman, fails in browserCORS headers missingAdd cors() middleware
Works for GET, fails for POSTPreflight not handledMove cors() before routes or add app.options('*', cors())
credentials: true still blockedUsing origin: '*'Switch to exact origin
Authorization header blockedNot in allowedHeadersAdd Authorization to allowedHeaders
Works locally, fails in productionHardcoded localhost originUse ALLOWED_ORIGIN env var

For CORS issues behind an nginx proxy, across multiple API subdomains, or when auth middleware runs before your CORS config, paste your Express setup into DebugAI. It traces the middleware chain and shows exactly where the headers are getting dropped.

Debug faster starting today.

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

Install Free →

Related Posts

Engineering

GitHub Copilot Just Changed Its Pricing. What Developers Need to Know

5 min read

Engineering

Why Your AI Agent Harness Fails at Debugging (And How to Fix It)

5 min read

← All posts