Skip to main content

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.

✓ Preflight request explained ✓ Express / FastAPI / Nginx ✓ Common errors & fixes ✓ Security best practices

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' headerServer not returning CORS headersAdd 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:trueUse explicit origin, not wildcard
The 'Access-Control-Allow-Origin' header contains multiple valuesProxy + app both adding headerAdd CORS in one layer only
Request header field authorization is not allowedAuthorization not in allowedHeadersAdd 'Authorization' to allowedHeaders
Response to preflight has invalid HTTP status code 404OPTIONS route not handledAdd 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.

Share this tool: X / Twitter LinkedIn
✓ 60,000+ developers use DevPlaybook ★★★★★ 4.8 / 5 on Gumroad ✓ MIT licensed · Instant download ✓ No login required

Related Products

on Gumroad

You Might Also Need

More Free Tools

Webhook Tester OpenAPI Validator WebSocket Tester WebP Converter WebSocket Test Client API Response Mock Generator API Response Time Tester OpenAPI Schema Validator OpenAPI Mock Generator API Test Case Generator Core Web Vitals Guide API Request Builder HTTP Security Headers Generator Web Vitals Scorer Web Vitals Analyzer API Mock Server Config Generator HTTP Security Headers Checker Core Web Vitals Budget Calculator API Rate Limit Calculator API Response Formatter & Analyzer HTTP API Tester PWA Manifest Generator LLM Cost Calculator WASM Module Explorer AI Prompt Cost Calculator REST API Request Builder Lighthouse Score Simulator Core Web Vitals Calculator Content Security Policy Builder LLM Prompt Cost Calculator API Response Mocker HTTP Header Inspector AI Rate Limit Calculator WebAssembly Binary Inspector Web Vitals Budget Planner Prompt Injection Tester GCP Service Account Key Auditor Web Performance Checklist Image Optimizer Checklist HTTP Headers Analyzer API Key Generator AI Doc Generator — Pro Tool AI System Guardian Color Blindness Simulator .htaccess Generator HTTP Methods Reference HTTP Code Snippet Generator HTTP Status Code Reference HTTP Status Code Reference Interview Question Generator JSON Diff Viewer JSON Diff JWT Builder JWT Decoder nginx Config Generator Password Strength Checker Rate Limiter Tester API Changelog Generator Service Worker Generator HTTP Mock Response Builder HMAC Generator CSP Header Builder Rate Limit Calculator CORS Policy Builder AWS IAM Policy Validator Haproxy Config Validator Caddy Caddyfile Generator WCAG Contrast Checker HTTP Cookie Parser SBOM Generator HTTP Status Code Lookup HTTP Status Code Lookup JWT Debugger Pro Vue Component Generator HTTP Status Code Reference LLM Context Window Calculator HTTP Header Analyzer LLM Token Counter Dockerfile Linter Bundle Size Estimator Prompt Token Estimator nginx Config Builder Mock Service Worker (MSW) API Mocking Guide Pulumi Modern IaC Guide Docker Layer Optimizer Bundle Size Impact Calculator HTML5 Canvas API Playground Token Context Calculator OpenAI Function Calling Generator TTFB Analyzer Token Counter & Cost Estimator AI API Rate Limit Calculator Password Strength Analyzer JWT Decoder & Validator

Enjoying this tool? Get more like it.

Weekly developer tool guides, productivity playbooks & AI tricks. Free. No spam.

DevPlaybook Pro 7 days free

Unlock AI-Powered Dev Tools

  • ⚡ AI Code Review, Doc Generator & SQL Builder
  • ⚡ All premium templates & early access
  • ⚡ Member discounts on Gumroad products
Start Free Trial →

$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.

$79
$241 separately Save $162 (67% off)
Get DevPlaybook Pro — $79 →

MIT licensed · Instant download · No subscription

See what's included → Browse all deals →