CORS (Cross-Origin Resource Sharing)

The browser's bouncer that decides which domains can talk to each other.

4 min read

What is CORS?

CORS is a security feature built into browsers that restricts web pages from making requests to domains different from the one serving the page. It's the reason your frontend on localhost:3000 can't just fetch data from api.example.com without permission.

The browser enforces this by checking specific HTTP headers on the server's response. If the headers don't explicitly allow your origin, the request fails.

Access to fetch at 'https://api.example.com/data' from origin
'http://localhost:3000' has been blocked by CORS policy

Sound familiar? Welcome to web development.

How It Works

  1. Browser makes a request from origin-a.com to origin-b.com
  2. Browser checks if origin-b.com allows requests from origin-a.com
  3. Server responds with CORS headers (or doesn't)
  4. Browser either allows or blocks the response

The key insight: CORS is enforced by the browser, not the server. The request actually reaches the server. The browser just won't let your JavaScript see the response if the headers are missing.

The Headers

HeaderPurposeExample
Access-Control-Allow-OriginWhich origins can access* or https://mysite.com
Access-Control-Allow-MethodsAllowed HTTP methodsGET, POST, PUT, DELETE
Access-Control-Allow-HeadersAllowed request headersContent-Type, Authorization
Access-Control-Allow-CredentialsAllow cookies/authtrue
Access-Control-Max-AgeCache preflight (seconds)86400
Access-Control-Expose-HeadersHeaders JS can readX-Custom-Header

Simple vs Preflight Requests

Not all cross-origin requests are created equal:

Simple requests go directly to the server:

  • GET, HEAD, or POST method
  • Only "safe" headers (Accept, Content-Type with simple values, etc.)
  • No custom headers

Preflight requests require an OPTIONS check first:

  • PUT, DELETE, PATCH methods
  • Custom headers like Authorization
  • Content-Type: application/json
OPTIONS /api/data HTTP/1.1
Origin: https://mysite.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://mysite.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

Server Configuration Examples

Express.js:

javascript
const cors = require('cors');

// Allow all origins (development only!)
app.use(cors());

// Allow specific origin
app.use(cors({
  origin: 'https://mysite.com',
  credentials: true
}));

Next.js API Route:

javascript
export async function GET(request) {
  return Response.json({ data: 'hello' }, {
    headers: {
      'Access-Control-Allow-Origin': 'https://mysite.com',
      'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    }
  });
}

Nginx:

nginx
location /api/ {
    add_header 'Access-Control-Allow-Origin' 'https://mysite.com';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
}

Where You'll Hit This

  • Frontend calling a backend API - The classic scenario
  • Third-party API integrations - Stripe, Twilio, etc.
  • Microservices architectures - Services on different subdomains
  • CDN resources - Fonts, images, scripts from other domains
  • Local development - Different ports count as different origins!

What Counts as "Cross-Origin"?

Two URLs have the same origin only if they match on:

  • Protocol (http vs https)
  • Host (api.site.com vs site.com)
  • Port (:3000 vs :8080)
FromToSame Origin?
https://site.comhttps://site.com/apiYes
https://site.comhttp://site.comNo (protocol)
https://site.comhttps://api.site.comNo (subdomain)
http://localhost:3000http://localhost:8080No (port)

Common Gotchas

⚠️Wildcard + Credentials Don't Mix

You cannot use Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true. The browser will reject it. You must specify the exact origin.

  • Forgetting the preflight - Your OPTIONS endpoint must also return CORS headers
  • Caching preflights - Set Access-Control-Max-Age to avoid repeated OPTIONS requests
  • Port matters in development - localhost:3000 and localhost:3001 are different origins
  • The request still reaches your server - CORS doesn't block the request, just the response. Don't rely on it for security.
ℹ️CORS is Not Security

CORS protects users, not your API. Anyone can bypass CORS using curl, Postman, or a backend proxy. Always authenticate and authorize requests server-side.

Debugging Tips

  1. Check the Network tab - Look for the OPTIONS preflight request
  2. Look at response headers - Are the CORS headers actually present?
  3. Check the console error - It usually tells you exactly what's missing
  4. Test with curl - If curl works but browser doesn't, it's CORS
bash
# This bypasses CORS (no browser)
curl -X POST https://api.example.com/data \
  -H "Content-Type: application/json" \
  -d '{"key": "value"}'

Try It

Encode URLs for Cross-Origin Requests

"CORS: Teaching developers that 'No' is a complete sentence since 2009."