If you have ever written a single line of frontend JavaScript, you have undoubtedly bumped into the infamous browser console error:
Access to fetch at 'https://api.example.com/data' from origin 'https://myapp.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
CORS (Cross-Origin Resource Sharing) is one of the most misunderstood mechanisms in modern web development. Frontend developers blame backend engineers, backend engineers configure wildcard wild-west headers, and security auditors panic.
In this definitive guide, we will unpack exactly how CORS works under the hood, why the Same-Origin Policy exists, and provide battle-tested, copy-paste configurations for Next.js, Node.js (Express), and Nginx.
What Exactly is CORS and the Same-Origin Policy (SOP)?
Before touching headers, you must understand the Same-Origin Policy (SOP). SOP is a fundamental browser security mechanism that prevents a malicious script on evil-hacker.com from making unauthorized HTTP requests to your online banking API at mybank.com using your logged-in cookies.
An origin is defined by three strict tuples:
- Protocol / Scheme (e.g.,
http://vs.https://) - Hostname / Domain (e.g.,
dailytoolbox.orgvs.api.dailytoolbox.org) - Port Number (e.g.,
:3000vs.:8080)
If any of these three elements differ between the requesting webpage and the target server, the browser treats the request as Cross-Origin.
Crucial Fact: CORS is enforced by the browser, not the server. Your server actually receives the request, executes the database query, and returns the response. However, when the browser inspects the response headers and sees no matching Access-Control-Allow-Origin, it discards the response data and throws a console error!
Simple Requests vs. Preflight OPTIONS Requests
Not all cross-origin requests trigger CORS checks in the same manner:
1. Simple Requests
A request qualifies as "Simple" if:
- It uses HTTP methods:
GET,POST, orHEAD. - It only contains standard headers like
Accept,Accept-Language,Content-Language, and standardContent-Type(application/x-www-form-urlencoded,multipart/form-data,text/plain).
For simple requests, the browser sends the request immediately with an Origin header.
2. Preflight Requests (The OPTIONS Ping)
If your request sends:
- Methods like
PUT,DELETE, orPATCH. - Custom headers like
Authorization: Bearer <token>orX-Custom-Header. Content-Type: application/json(common across 99% of modern REST APIs).
The browser first pauses your real request and sends an invisible preflight HTTP OPTIONS probe to ask the server: "Are you willing to accept a JSON request from this origin with an Authorization header?"
If the server does not handle the OPTIONS method with HTTP 200/204 and appropriate CORS headers, the actual request is never sent.
How to Fix CORS Across Different Tech Stacks
Solution 1: Next.js (App Router & Route Handlers)
If your API is hosted inside a Next.js 14/15 App Router project, you can configure global CORS headers in next.config.mjs:
// next.config.mjs
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
// Apply CORS headers to all routes under /api/
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "https://yourfrontend.com" }, // Avoid * when credentials are true
{ key: "Access-Control-Allow-Methods", value: "GET,DELETE,PATCH,POST,PUT,OPTIONS" },
{ key: "Access-Control-Allow-Headers", value: "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version, Authorization" },
]
}
];
}
};
export default nextConfig;
If you have a dedicated route handler (app/api/data/route.ts), remember to export an explicit OPTIONS handler:
export async function OPTIONS() {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "https://yourfrontend.com",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
Solution 2: Express.js (Node.js Backend)
Instead of manually appending headers to every endpoint, use the battle-tested cors npm package:
npm install cors
const express = require('express');
const cors = require('cors');
const app = express();
const whitelist = ['https://dailytoolbox.org', 'https://dev.dailytoolbox.org'];
const corsOptions = {
origin: function (origin, callback) {
// Allow non-browser tools (e.g. curl, mobile apps, Postman) where origin is undefined
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Blocked by CORS policy'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
};
app.use(cors(corsOptions));
Solution 3: Nginx Reverse Proxy (The Cleanest Architecture)
Configuring CORS at the reverse proxy layer (Nginx) takes the burden off your application code entirely:
# /etc/nginx/conf.d/api.conf
location /api/ {
# Set CORS headers
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_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' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '$http_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' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization' always;
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
3 Critical Security Pitfalls to Avoid
- Never use
Access-Control-Allow-Origin: *withAccess-Control-Allow-Credentials: true: Browsers explicitly forbid this combination for security reasons. If you need credentials (cookies/sessions), you must echo back the exact validated origin. - CORS is NOT Authentication or Authorization:
Blocking an origin does not stop hackers from sending requests via Postman, Python scripts, or
curl. Always protect your APIs with JWT tokens, session checks, and rate limiters. - Inspect Your API Response Headers with Developer Tools:
Whenever debugging, open Chrome DevTools (
F12), go to the Network tab, click on the failed request, and check both the Request Headers (what origin you sent) and Response Headers (what the server responded with). You can also test your API payloads using the free DailyToolbox Developer Suite.
Summary
CORS errors are not mysterious bugs; they are your browser's way of protecting users. By ensuring your server handles the OPTIONS preflight properly and returns the correct Access-Control-Allow-Origin headers, you can build seamless, secure multi-origin web architectures.