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.
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.
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 │
└───────────────────────────────────────┴────────────────────────────────────────┴────────────────────────────────────────────┘