CORS: The Complete Handbook for Modern Web APIs

CORS: The Complete Handbook for Modern Web APIs#

Cross-Origin Resource Sharing, or CORS, is one of the most misunderstood parts of web development. Teams lose hours to it because the browser error messages feel vague, framework defaults vary wildly, and many blog posts reduce the topic to β€œjust add Access-Control-Allow-Origin: *”.

That advice is often wrong.

CORS is not an authentication system, not a CSRF defense, and not a server-to-server access control mechanism. It is a browser-enforced policy layer that decides whether frontend JavaScript running on one origin may read a response from another origin.

CORS in Nginx and Apache: Configurations That Actually Work

Setting CORS at the web server level is often the cleanest approach. Your application doesn’t need to know about CORS at all β€” Nginx or Apache handles it before the request even reaches your app.

Here are configurations I’ve used in production that work.

Nginx#

Basic: Single Origin#

server {
    listen 80;
    server_name api.example.com;
    
    location / {
        add_header Access-Control-Allow-Origin "https://myapp.com";
        add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
        add_header Access-Control-Allow-Headers "Content-Type, Authorization";
        add_header Access-Control-Allow-Credentials "true";
        add_header Access-Control-Max-Age "86400";
        add_header Access-Control-Expose-Headers "X-Total-Count, X-Request-Id";
        
        # Handle preflight
        if ($request_method = OPTIONS) {
            add_header Access-Control-Allow-Origin "https://myapp.com";
            add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS";
            add_header Access-Control-Allow-Headers "Content-Type, Authorization";
            add_header Access-Control-Allow-Credentials "true";
            add_header Access-Control-Max-Age "86400";
            return 204;
        }
        
        proxy_pass http://127.0.0.1:3000;
    }
}

Note: You need to repeat the add_header directives inside the if block because Nginx’s if directive creates a new context. Headers set outside the if don’t apply inside it. This is a well-known Nginx gotcha.

CORS Preflight Requests: What They Are and Why Your API Needs to Handle Them

Every time your React app sends a JSON POST request, the browser does something you might not expect: it sends TWO requests instead of one. The first is an OPTIONS “preflight” request. The second is your actual request.

This confuses a lot of people. Why is the browser sending extra requests? Why is my API getting OPTIONS requests I never wrote endpoints for? Why does Postman work but the browser doesn’t?

CORS vs Same-Origin Policy: They're Not the Same Thing

I’ve heard developers say “I need to add CORS to my API for security” more times than I can count. That’s backwards. CORS doesn’t make your API more secure. In fact, it makes it less restricted. The security feature is the Same-Origin Policy. CORS is the controlled exception.

Let me clear this up once and for all.

Same-Origin Policy (SOP)#

The Same-Origin Policy is a built-in browser security mechanism. It’s been around since the early days of the web. Here’s what it does:

Every CORS Header Explained (With Real Request/Response Examples)

This is the page I keep coming back to when I need to remember the exact syntax or behavior of a CORS header. I’m putting it all in one place so you don’t have to hunt through MDN and Stack Overflow.

Response Headers (What Your Server Sends)#

These are the headers your API server needs to send. The browser reads these to decide whether to allow the cross-origin request.

Access-Control-Allow-Origin#

The single most important CORS header. Without it, nothing works.

Setting Up CORS in Node.js and Express: From Basic to Production-Ready

Express makes CORS relatively painless, but there are a few gotchas that catch people off guard. Let me walk through every setup I’ve seen work in production.

The cors Package (Easiest Option)#

npm install cors

The One-Liner (Development Only)#

const cors = require('cors');
app.use(cors());

This allows all origins, all methods, all headers. Fine for local development. Do NOT use this in production.

Allow a Single Origin#

const cors = require('cors');

app.use(cors({
  origin: 'https://myapp.com',
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400,
}));

Allow Multiple Origins#

This is where it gets slightly tricky. The cors package doesn’t accept an array for origin β€” it accepts a function:

The 7 CORS Errors You'll See Most (And Exactly How to Fix Each One)

I’ve fixed more CORS errors than I can count. They all look slightly different but most fall into the same handful of categories. Here are the seven you’ll see most often, with the exact fix for each.

Error 1: “No ‘Access-Control-Allow-Origin’ header is present”#

Access to fetch at 'https://api.example.com/users' from origin 
'https://myapp.com' has been blocked by CORS policy: No 
'Access-Control-Allow-Origin' header is present on the requested resource.

This is the most common CORS error. It means your server isn’t sending any CORS headers at all.

What Is CORS and Why Is It Ruining Your API Calls?

You’ve built a React frontend. You’ve built a Node.js API. They work perfectly when you test them separately. You wire them together, make your first API call, and…

Access to fetch at 'http://localhost:3001/api/users' from origin 
'http://localhost:3000' has been blocked by CORS policy: 
No 'Access-Control-Allow-Origin' header is present on the requested resource.

Sound familiar? Every developer hits this wall. And most developers respond by Googling “how to fix CORS” and pasting app.use(cors()) without understanding what they just did.