CORS Configuration Guide for REST APIs
A practical guide to understanding and correctly configuring Cross-Origin Resource Sharing for REST APIs. Covers the browser mechanics, common misconfigurations, and production-ready code for Node.js, Python, and Nginx.
How CORS Works: The Browser Flow
When your frontend at https://app.example.com calls an API at https://api.example.com, those are different origins (different subdomain). The browser enforces CORS to protect users.
── Simple Request Flow ──────────────────────────────────────────────
Browser → API: GET /data
Origin: https://app.example.com
API → Browser: 200 OK
Access-Control-Allow-Origin: https://app.example.com
✅ Browser allows JavaScript to read the response
── Preflight Flow (non-simple request) ──────────────────────────────
Browser → API: OPTIONS /data
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: Authorization
API → Browser: 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400 ← cache preflight for 1 day
Browser → API: DELETE /data/123
Authorization: Bearer eyJ...
✅ Now sends the real request Express.js Configuration
Simple Single-Origin Setup
import express from 'express';
import cors from 'cors';
const app = express();
// Simple: single trusted origin
app.use(cors({
origin: 'https://app.yourcompany.com',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
exposedHeaders: ['X-Total-Count', 'X-Page'], // headers JS can read
maxAge: 86400, // preflight cache: 24 hours
}));
app.options('*', cors()); // enable pre-flight for all routes Multi-Origin with Dynamic Allowlist
const ALLOWED_ORIGINS = new Set([
'https://app.yourcompany.com',
'https://admin.yourcompany.com',
'https://staging.yourcompany.com',
...(process.env.NODE_ENV === 'development' ? ['http://localhost:3000', 'http://localhost:5173'] : []),
]);
app.use(cors({
origin: (origin, callback) => {
// Allow server-to-server requests (no origin header)
if (!origin) return callback(null, true);
if (ALLOWED_ORIGINS.has(origin)) {
callback(null, true);
} else {
console.warn(`CORS blocked: ${origin}`);
callback(new Error(`Origin ${origin} not allowed`));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// ⚠️ When using dynamic origin, always add Vary: Origin
app.use((req, res, next) => {
res.vary('Origin');
next();
}); FastAPI Configuration
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import os
app = FastAPI()
ALLOWED_ORIGINS = [
"https://app.yourcompany.com",
"https://admin.yourcompany.com",
]
if os.getenv("ENVIRONMENT") == "development":
ALLOWED_ORIGINS.extend([
"http://localhost:3000",
"http://localhost:5173",
])
app.add_middleware(
CORSMiddleware,
allow_origins=ALLOWED_ORIGINS, # never use ["*"] with credentials
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Authorization", "Content-Type", "X-Request-ID"],
expose_headers=["X-Total-Count"],
max_age=86400,
) Nginx Configuration
server {
listen 443 ssl;
server_name api.yourcompany.com;
# Map origins to allowed flag
map $http_origin $cors_origin {
default "";
"https://app.yourcompany.com" $http_origin;
"https://admin.yourcompany.com" $http_origin;
}
location /api/ {
# Handle preflight
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization,Content-Type" always;
add_header Access-Control-Max-Age 86400 always;
add_header Vary Origin always;
return 204;
}
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always;
proxy_pass http://backend;
}
} Common CORS Errors & Fixes
| Error Message | Root Cause | Fix |
|---|---|---|
| No 'Access-Control-Allow-Origin' header | Server not returning CORS headers | Add CORS middleware / nginx config |
| has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include' | Using * with credentials:true | Use explicit origin, not wildcard |
| The 'Access-Control-Allow-Origin' header contains multiple values | Proxy + app both adding header | Add CORS in one layer only |
| Request header field authorization is not allowed | Authorization not in allowedHeaders | Add 'Authorization' to allowedHeaders |
| Response to preflight has invalid HTTP status code 404 | OPTIONS route not handled | Add app.options('*', cors()) |
Frequently Asked Questions
What is CORS and why do browsers enforce it?▼
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that restricts web pages from making requests to a different origin (protocol + domain + port) than the one that served the page. Browsers enforce it to prevent malicious websites from making authenticated requests to other sites on behalf of logged-in users (CSRF-style attacks). CORS is browser-enforced — server-to-server requests, curl, and Postman are not subject to CORS.
What is a preflight request?▼
A preflight is an automatic OPTIONS request the browser sends before a 'non-simple' cross-origin request. Simple requests use GET/HEAD/POST with basic headers and standard content types. Preflight is triggered for: custom headers (like Authorization), methods other than GET/HEAD/POST, Content-Type other than application/x-www-form-urlencoded, multipart/form-data, or text/plain. The server must respond with appropriate Access-Control-Allow-* headers for the browser to proceed with the actual request.
Why does my CORS config work in development but fail in production?▼
Common causes: (1) Your production domain is not in the allowlist — check for http vs https or www vs non-www mismatches. (2) A reverse proxy (nginx, load balancer) is adding duplicate CORS headers — both the proxy and app are setting headers, causing 'multiple values' errors. (3) Your CDN is caching OPTIONS responses without Vary: Origin. (4) Environment variables for allowed origins are not set in production. Always log the incoming Origin header and the outgoing CORS headers to debug.
Can I use Access-Control-Allow-Origin: * with credentials?▼
No — this combination is explicitly forbidden by the CORS specification and browsers will reject it. When credentials: true (cookies, Authorization headers) you must specify an explicit origin, not a wildcard. If you need to support multiple origins, maintain an allowlist and dynamically respond with the exact requesting Origin if it's in the list. Always include Vary: Origin in responses when doing dynamic origin matching.
What is the difference between CORS and CSRF?▼
CORS controls whether a browser allows a cross-origin response to be read by JavaScript. CSRF (Cross-Site Request Forgery) tricks a victim's browser into making a request to another site — the browser sends cookies automatically. CORS does not fully prevent CSRF: a simple POST with standard headers won't trigger a preflight. Use CSRF tokens or SameSite=Strict cookies for CSRF prevention. They are complementary protections, not alternatives.
How do I configure CORS for a WebSocket connection?▼
WebSockets don't use CORS — they perform a regular HTTP upgrade request, but browsers don't enforce CORS on WebSocket connections. Instead, validate the Origin header manually in your WebSocket upgrade handler. If the Origin is not in your allowlist, reject the handshake with a 403. This is critical because browsers automatically include cookies with WebSocket connections to authenticated origins.
Related Products
on GumroadDevPlaybook Interview Vault
210+ real technical interview questions with model answers, asked at Google, Meta, Amazon, Microsoft, Stripe, and Shopify. Covers Frontend (55), Backend (55), System Design (50), and DSA (50). Available in English and Traditional Chinese.
Buy on Gumroad →TypeScript Quick Reference Toolkit
5 practical reference files for TypeScript developers: cheatsheet, TS vs JS comparison, 20 utility types with real examples, tsconfig best practices template, and 15 common mistakes guide. Print it. Keep it on your desk.
Buy on Gumroad →Developer Interview Vault
210 technical interview questions with model answers, key points, and follow-up questions. Covers Frontend, Backend, System Design, and DSA — bilingual English + Traditional Chinese (ZH-TW). PWYW from $29.
Buy on Gumroad →You Might Also Need
Webhook Tester
Generate realistic webhook test payloads for GitHub, Stripe, Slack, and custom services. Get a curl command to send the payload to your local endpoint. Inspect headers, payload size, and content-type.
Open free tool →
OpenAPI Validator
Paste your OpenAPI 3.x spec (YAML or JSON) to validate structure, find missing required fields, detect duplicate paths, check for missing operationIds, and get errors and warnings with path references.
Open free tool →
WebSocket Tester
Connect to any WebSocket server, send and receive messages in real time, and inspect the message log with timestamps and byte sizes. Free online WebSocket client.
Open free tool →
More Free Tools
Related Articles
API Rate Limiting: Algorithms and Implementation Guide 2026
Complete guide to API rate limiting in 2026. Token Bucket, Leaky Bucket, Sliding Window algorithms with Node.js and Redis implementations, distributed rate limiting, 429 handling, and best practices.
Base64 Encoding Explained: When to Use It and When to Avoid It
Base64 encoding explained: when to use it in APIs, image data URIs, and auth tokens. Real-world examples and common pitfalls for web developers.
API Rate Limiting: Strategies, Algorithms, and Free Tools
A complete guide to API rate limiting in 2025: Fixed Window, Sliding Window, Token Bucket, and Leaky Bucket algorithms explained, with implementation in Node.js, Python, and Go, Redis-based distributed limiting, and client-side backoff strategies.
The Ultimate API Design Checklist for Production Apps
Ship production-ready APIs with this comprehensive checklist. Covers REST design, authentication, validation, error handling, rate limiting, versioning, documentation, and monitoring.
FastAPI vs Django REST Framework 2025: Which Should You Choose?
FastAPI vs Django REST Framework compared for 2025. Performance benchmarks, feature comparison, real-world use cases, and a clear decision guide for your next Python API project.
Enjoying this tool? Get more like it.
Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.
Unlock AI-Powered Dev Tools
- ⚡ AI Code Review, Doc Generator & SQL Builder
- ⚡ All premium templates & early access
- ⚡ Member discounts on Gumroad products
$9/mo after trial · Cancel anytime
Want the full toolkit?
Get DevPlaybook Pro
Every template, guide, boilerplate, and automation script in one bundle. 13 premium products — grab them all at once and save big.
MIT licensed · Instant download · No subscription
See what's included → Browse all deals →