CORS: The Error Every Developer Has Seen
You're building a frontend that calls your API. It works in Postman. It works with curl. But the browser says:
Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.This isn't a server error. It's the browser protecting the user.
What CORS Actually Is
CORS (Cross-Origin Resource Sharing) is a browser security mechanism. By default, JavaScript running on origin A cannot make requests to origin B. An origin is the combination of protocol + domain + port:
| URL | Origin |
|---|---|
http://localhost:3000 | http://localhost:3000 |
https://myapp.com | https://myapp.com |
https://api.myapp.com | https://api.myapp.com (different subdomain = different origin) |
https://myapp.com:8080 | https://myapp.com:8080 (different port = different origin) |
How It Works
For simple requests (GET, POST with standard content types), the browser sends the request normally but checks the response headers. If the response doesn't include Access-Control-Allow-Origin, the browser blocks JavaScript from reading the response.
For complex requests (PUT, DELETE, custom headers, JSON content type), the browser sends a preflight request first:
OPTIONS /api/data HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, AuthorizationThe server must respond with:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400Fixing CORS
Express.js:
import cors from 'cors';
// Allow all origins (development only!)
app.use(cors());
// Production: specify allowed origins
app.use(cors({
origin: ['https://myapp.com', 'https://staging.myapp.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true, // if using cookies
}));Next.js API routes:
export async function GET(request: Request) {
const data = await getData();
return Response.json(data, {
headers: {
'Access-Control-Allow-Origin': 'https://myapp.com',
'Access-Control-Allow-Methods': 'GET, POST',
},
});
}Nginx:
location /api/ {
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';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
return 204;
}
}Common Mistakes
- Using
*with credentials.Access-Control-Allow-Origin: *doesn't work withcredentials: true. You must specify the exact origin. - Forgetting the OPTIONS handler. Your server must respond to OPTIONS preflight requests with CORS headers and a 204 status.
- Fixing it on the frontend. CORS is a server-side configuration. No amount of frontend code can bypass it (that's the point).
- Proxying in dev, forgetting in prod.
next.config.jsrewrites work in development but not in production. You need actual CORS headers on the API server.
Alternatives to CORS
- Same-origin API proxy: Route API calls through your own server (
/api/proxy) so the browser sees same-origin requests. - Server-side rendering: Fetch data on the server (Next.js Server Components), not in the browser. No CORS needed.
Debug your headers: HTTP Reference — check status codes and headers for your API responses.