You’re building a frontend that calls your API, and suddenly:

Ahocancsetsbhseeetnroebqflueoetcscktheeddatbrye'shCotOutRrpSc:e/p./olloiccayl:hoNsot:'8A0c0c0e/sasp-iC/odnattrao'l-fArlolmowo-rOirgiignin''hthtepa:d/e/rloicsalphroesste:n3t000'

Your API works fine in Postman. It works with curl. But your browser refuses to load the data. What’s going on?

Why CORS Exists

CORS (Cross-Origin Resource Sharing) is a browser security feature, not a server bug. It prevents malicious websites from making requests to APIs on your behalf.

An “origin” is the combination of protocol, domain, and port:

  • http://localhost:3000 (your React app)
  • http://localhost:8000 (your API)

These are different origins because the ports differ. The browser blocks cross-origin requests by default unless the server explicitly allows them.

Key insight: CORS is enforced by browsers, not servers. That’s why curl works—it doesn’t care about CORS.

The Fix: Add CORS Headers

Your server needs to tell the browser “yes, this origin is allowed to access me.”

Node.js / Express

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const cors = require('cors');

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

// Or specify allowed origins (production)
app.use(cors({
  origin: ['http://localhost:3000', 'https://myapp.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  credentials: true  // if you need cookies
}));

Python / FastAPI

1
2
3
4
5
6
7
8
9
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "https://myapp.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

Python / Flask

1
2
3
4
5
6
7
from flask_cors import CORS

# Allow all origins
CORS(app)

# Or specify origins
CORS(app, origins=["http://localhost:3000"])

Nginx (Reverse Proxy)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
location /api/ {
    proxy_pass http://backend:8000/;
    
    # CORS headers
    add_header 'Access-Control-Allow-Origin' '$http_origin' 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-Allow-Credentials' 'true' always;
    
    # Handle preflight
    if ($request_method = 'OPTIONS') {
        return 204;
    }
}

The Preflight Problem

If you’re sending custom headers (like Authorization) or using methods other than GET/POST, the browser sends a preflight OPTIONS request first.

Your server must handle this:

1
2
3
4
5
6
7
8
// Express - cors middleware handles this automatically
// But if you're doing it manually:
app.options('*', (req, res) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
  res.header('Access-Control-Allow-Headers', 'Authorization, Content-Type');
  res.sendStatus(204);
});

Symptom: Your GET requests work but POST/PUT fail, or requests with Authorization headers fail. Check if OPTIONS requests are returning proper CORS headers.

Common Mistakes

1. Wildcard with Credentials

This doesn’t work:

1
2
3
4
5
// WRONG - browser will reject this
app.use(cors({
  origin: '*',
  credentials: true
}));

If you need credentials: true (for cookies), you must specify exact origins, not *.

2. Missing Headers in Error Responses

CORS headers must be present on all responses, including errors. If your 500 error page doesn’t have CORS headers, the browser shows a CORS error instead of the actual error.

1
2
3
4
5
// Make sure error handlers also have CORS
app.use((err, req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.status(500).json({ error: err.message });
});

3. Protocol Mismatch

http://localhost:3000 and https://localhost:3000 are different origins. Make sure your allowed origins match exactly what your frontend uses.

Quick Debugging Checklist

  1. Check the actual response headers - Open DevTools → Network → click the failed request → Headers tab. Is Access-Control-Allow-Origin present?

  2. Check for preflight - Look for an OPTIONS request before your actual request. Is it returning 200/204 with proper headers?

  3. Check your origin exactly - Copy the error message’s origin and make sure it matches your allowed origins list exactly.

  4. Test without credentials first - Remove credentials: 'include' from your fetch and credentials: true from your server to isolate the issue.

The “Just Make It Work” Development Fix

During local development, if you just need it to work:

1
2
3
4
5
6
7
8
9
// server.js - DEVELOPMENT ONLY
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin);
  res.header('Access-Control-Allow-Methods', '*');
  res.header('Access-Control-Allow-Headers', '*');
  res.header('Access-Control-Allow-Credentials', 'true');
  if (req.method === 'OPTIONS') return res.sendStatus(204);
  next();
});

Never deploy this to production. It allows any website to call your API.

Production Checklist

  • Specify exact allowed origins (no wildcards with credentials)
  • Handle OPTIONS preflight requests
  • Include CORS headers on error responses
  • Test with actual production domains before deploying

CORS errors are frustrating because the browser hides the real response. But once you understand it’s just missing headers, the fix is straightforward.