
If you have built a web application that calls an API on a different domain, you have almost certainly encountered a CORS error. The browser blocks your request, the console shows a cryptic message about “Access-Control-Allow-Origin,” and your first instinct is to set origin: '*' and move on. However, understanding what CORS actually does — and why the browser enforces it — is essential for configuring it correctly without opening your application to security risks.
CORS (Cross-Origin Resource Sharing) is a browser security mechanism that controls which domains can make requests to your server. This guide explains the mental model behind CORS, walks through how preflight requests work, and shows you how to configure CORS properly in production. It also covers the mistakes that developers make most frequently and why the quick fixes you find on Stack Overflow often create bigger problems than they solve.
What Is CORS and Why Does It Exist?
CORS explained in one sentence: it is a set of HTTP headers that tells the browser whether a web page on one domain is allowed to make requests to a server on a different domain.
By default, browsers enforce the Same-Origin Policy, which prevents JavaScript on https://myapp.com from making requests to https://api.example.com. This policy exists because without it, any website you visit could silently make authenticated requests to your bank, your email, or any other service where you are logged in — using your cookies and session tokens.
CORS relaxes the Same-Origin Policy in a controlled way. Instead of blocking all cross-origin requests, the server declares which origins are allowed, what HTTP methods they can use, and whether cookies or authentication headers are permitted. The browser reads these declarations and decides whether to allow the request.
The Mental Model: A Bouncer at the Door
Think of CORS as a bouncer at a nightclub. The browser (the bouncer) checks every cross-origin request against the server’s CORS policy (the guest list). If the requesting origin is on the list, the request goes through. If not, the browser blocks it — even though the server might have already processed the request and sent a response.
This is a critical detail: CORS is enforced by the browser, not the server. The server sets the rules via HTTP headers, but only browsers respect those rules. Consequently, tools like curl, Postman, and server-to-server requests bypass CORS entirely because they are not browsers. This means CORS protects browser-based users, not your API itself.
How CORS Works: The Request Flow
CORS handles requests differently depending on their complexity. Simple requests go straight through, while complex requests trigger a preflight check first.
Simple Requests
A request is “simple” if it meets all of these conditions: it uses GET, HEAD, or POST; it only includes standard headers (Accept, Accept-Language, Content-Language, Content-Type); and its Content-Type is application/x-www-form-urlencoded, multipart/form-data, or text/plain.
For simple requests, the browser sends the request directly and checks the response headers afterward.
GET /api/products HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
--- Server Response ---
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Content-Type: application/json
[{"id": 1, "name": "Widget"}]
The browser sees that Access-Control-Allow-Origin matches the requesting origin and allows the JavaScript to access the response. If the header is missing or does not match, the browser blocks the response — even though the server already returned the data.
Preflight Requests
Any request that does not qualify as “simple” triggers a preflight. This includes requests with Content-Type: application/json (the most common case), custom headers like Authorization, or HTTP methods like PUT, PATCH, or DELETE.
The browser sends an OPTIONS request before the actual request to ask the server whether the real request is allowed.
OPTIONS /api/orders HTTP/1.1
Host: api.example.com
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
--- Server Response ---
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Only after the preflight succeeds does the browser send the actual POST request. The Access-Control-Max-Age header tells the browser to cache the preflight response for 86,400 seconds (24 hours), so subsequent requests to the same endpoint skip the preflight.
Credentialed Requests
By default, cross-origin requests do not include cookies or authentication headers. To send credentials, both sides must opt in. The client sets credentials: 'include' on the fetch request, and the server responds with Access-Control-Allow-Credentials: true.
// Client-side: include credentials
const response = await fetch('https://api.example.com/api/profile', {
credentials: 'include',
});
--- Server Response ---
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Credentials: true
Importantly, when credentials are included, the server cannot use Access-Control-Allow-Origin: *. It must specify the exact origin. This restriction exists because a wildcard with credentials would allow any website to make authenticated requests to your API — defeating the purpose of CORS entirely.
Configuring CORS in Production
The configuration approach depends on your backend framework. Here are production-ready configurations for the most common setups.
Express.js (Node.js)
import cors from 'cors';
import express from 'express';
const app = express();
const allowedOrigins = [
'https://myapp.com',
'https://admin.myapp.com',
process.env.NODE_ENV === 'development' && 'http://localhost:3000',
].filter(Boolean);
app.use(cors({
origin: (origin, callback) => {
// Allow requests with no origin (mobile apps, curl, server-to-server)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
}));
This configuration uses a dynamic origin check rather than a static string. As a result, you can support multiple allowed origins while still returning the specific origin in the response header (required for credentialed requests). For teams building scalable Express.js applications, placing CORS configuration in middleware keeps it centralized and maintainable.
Spring Boot (Java)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("https://myapp.com");
config.addAllowedOrigin("https://admin.myapp.com");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
config.setMaxAge(86400L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return new CorsFilter(source);
}
}
For Spring Boot applications with JWT security, ensure CORS configuration is processed before Spring Security filters, or the preflight OPTIONS request will be rejected with a 401 before it reaches your CORS handler.
FastAPI (Python)
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
allowed_origins = [
"https://myapp.com",
"https://admin.myapp.com",
]
app.add_middleware(
CORSMiddleware,
allow_origins=allowed_origins,
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
allow_headers=["Content-Type", "Authorization"],
max_age=86400,
)
FastAPI’s built-in CORS middleware handles preflight requests automatically. The allow_origins list should contain exact origin URLs including the protocol — https://myapp.com, not myapp.com.
Common CORS Mistakes
These mistakes appear in production codebases regularly. Understanding why each one is problematic helps you avoid them.
Mistake 1: Using origin: '*' with Credentials
// BROKEN: Wildcard origin with credentials
app.use(cors({
origin: '*',
credentials: true, // This combination is invalid
}));
The browser rejects this configuration. When Access-Control-Allow-Credentials is true, the Access-Control-Allow-Origin header must be a specific origin, not *. Consequently, this configuration causes requests with cookies to fail silently.
Mistake 2: Reflecting the Origin Header Without Validation
// INSECURE: Reflecting any origin that asks
app.use(cors({
origin: (origin, callback) => {
callback(null, origin); // Allows EVERY origin
},
credentials: true,
}));
This is functionally identical to origin: '*' but bypasses the browser’s wildcard-with-credentials restriction. Any website can now make authenticated requests to your API. This pattern effectively disables CORS protection entirely.
Mistake 3: Forgetting the OPTIONS Handler
Some server configurations do not handle OPTIONS requests automatically. If your preflight returns a 404 or 405, every non-simple cross-origin request fails.
// If not using the cors package, handle OPTIONS manually
app.options('/api/*', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'https://myapp.com');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
res.status(204).end();
});
Using the cors npm package or your framework’s built-in CORS middleware handles this automatically. Nevertheless, if you configure CORS manually or use a reverse proxy, verify that OPTIONS requests reach your CORS handler.
Mistake 4: Configuring CORS in the Wrong Layer
When your application sits behind a reverse proxy (Nginx, Cloudflare, API Gateway), CORS headers can be set at multiple layers. If both your application and Nginx add Access-Control-Allow-Origin, the browser receives duplicate headers and rejects the response.
# Nginx — only add CORS headers here if your app does NOT set them
location /api/ {
proxy_pass http://backend:3000;
# Remove if your application already sets CORS headers
add_header Access-Control-Allow-Origin "https://myapp.com" always;
}
Choose one layer to manage CORS headers and ensure other layers pass the headers through without modification.
Mistake 5: Not Including Vary: Origin
When your server returns different Access-Control-Allow-Origin values depending on the requesting origin, you must include Vary: Origin in the response. Without it, a CDN or browser cache might serve a response with the wrong origin header to a different requester.
app.use((req, res, next) => {
res.setHeader('Vary', 'Origin');
next();
});
The cors npm package handles this automatically. However, if you configure CORS manually or through a proxy, add Vary: Origin explicitly.
Debugging CORS Errors
When you encounter a CORS error, the browser console message tells you what went wrong. Here is how to interpret the most common errors.
“No ‘Access-Control-Allow-Origin’ header is present” — The server did not include the CORS header in its response. Either CORS is not configured on the server, or the origin is not in the allowlist.
“The value of the ‘Access-Control-Allow-Origin’ header must not be the wildcard ‘*'” — You are sending credentials with a wildcard origin. Specify the exact origin instead.
“Method PUT is not allowed” — The preflight response did not include PUT in Access-Control-Allow-Methods. Add it to your configuration.
“Request header ‘authorization’ is not allowed” — The preflight response did not include Authorization in Access-Control-Allow-Headers. Add it explicitly.
For deeper debugging, check the Network tab in your browser’s DevTools. Look for the OPTIONS preflight request and inspect its response headers. If the preflight returns a non-2xx status code, the actual request will never be sent.
When to Use Permissive CORS
- Public APIs with no authentication —
origin: '*'is appropriate because there are no credentials to protect - Development environments — allow
localhostorigins for local development, but never deploy this to production - CDN-hosted static assets — fonts, images, and scripts served from a CDN typically use
origin: '*'
When NOT to Use Permissive CORS
- Any API that accepts cookies or authentication headers — always specify exact origins
- APIs that modify data — restrict to known frontends that should have write access
- Internal APIs — if only your own frontend should access the API, allowlist only that origin
- APIs behind authentication like OAuth2 or JWT tokens — wildcard origins combined with credentials create a security hole
Common Misconceptions About CORS
“CORS protects my API from unauthorized access.” CORS only affects browser-based requests. Server-to-server calls, curl, and mobile apps bypass CORS entirely. To protect your API, use authentication and authorization — not CORS alone.
“Setting origin: '*' is always insecure.” For truly public APIs with no authentication, a wildcard origin is perfectly fine. The security concern arises only when credentials are involved.
“Preflight requests slow down my application.” The Access-Control-Max-Age header lets browsers cache preflight responses. With a 24-hour cache, the preflight overhead only occurs once per endpoint per day.
“CORS errors come from the server.” CORS errors are enforced by the browser. The server often processes the request successfully — the browser simply hides the response from JavaScript because the CORS headers are missing or incorrect.
Making CORS Work in Production
CORS configuration is straightforward once you understand the mechanism. Define an explicit list of allowed origins rather than using wildcards. Enable credentials only when your frontend needs to send cookies or authorization headers. Handle preflight requests through your framework’s CORS middleware rather than manually. Cache preflight responses with Access-Control-Max-Age to minimize overhead. Configure CORS at exactly one layer in your infrastructure to avoid duplicate headers.
When CORS is configured correctly, it operates invisibly — your frontend communicates with your API, and the browser handles the security checks without any visible impact on the user experience. The errors only surface when the configuration is wrong, which is why understanding the mechanism is more valuable than memorizing the headers.