A comprehensive guide to Cross-Origin Resource Sharing — how it works, why it exists, and practical solutions for every CORS error you'll encounter.
CORS errors are one of the most common frustrations in web development. You make an API call, and suddenly the browser throws a red error: "Access to fetch at 'https://api.example.com' from origin 'http://localhost:3000' has been blocked by CORS policy." This guide explains why this happens and how to fix it correctly.
CORS (Cross-Origin Resource Sharing) is a browser security feature that restricts HTTP requests made from JavaScript in a web page to a different origin than the page's own origin.
An origin is the combination of:
http:// vs https://example.com vs api.example.com:3000 vs :8080Any difference in these three makes it "cross-origin":
| Request from | To | Cross-origin? |
|---|---|---|
http://example.com | https://example.com | ✅ Yes (different protocol) |
http://example.com | http://api.example.com | ✅ Yes (different subdomain) |
http://example.com:3000 | http://example.com:4000 | ✅ Yes (different port) |
http://example.com/page1 | http://example.com/api | ❌ No (same origin) |
Without CORS, a malicious website could use your logged-in session to make requests to your bank's API:
// Without CORS, evil.com could do this:
fetch('https://yourbank.com/api/transfer', {
method: 'POST',
credentials: 'include', // sends your bank cookies
body: JSON.stringify({ to: 'attacker', amount: 10000 })
});
CORS prevents this by having the server declare which origins are allowed to make requests to it.
Some requests are "simple" (GET/HEAD/POST with common headers and content types). The browser sends the request with an Origin header and checks the response:
Request: Origin: http://localhost:3000
Response: Access-Control-Allow-Origin: http://localhost:3000 ✅ allowed
Response: Access-Control-Allow-Origin: * ✅ allowed
Response: (no header) ❌ blocked
For requests with custom headers, methods like PUT/DELETE, or non-standard content types, the browser sends a preflight OPTIONS request first:
OPTIONS /api/users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization, content-type
HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: authorization, content-type
Access-Control-Max-Age: 86400
If the preflight fails, the actual request is never sent.
The correct fix is always server-side. Never add a browser extension or proxy hack in production.
const cors = require('cors');
// Allow all origins (development only!)
app.use(cors());
// Allow specific origins
app.use(cors({
origin: ['https://yourapp.com', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Authorization', 'Content-Type'],
credentials: true, // if you need cookies/auth
}));
// app/api/data/route.ts
export async function GET(req: Request) {
const data = await getData();
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'https://yourapp.com',
},
});
}
// Handle preflight
export async function OPTIONS() {
return new Response(null, {
headers: {
'Access-Control-Allow-Origin': 'https://yourapp.com',
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
});
}
// middleware.ts
import { NextResponse } from 'next/server';
export function middleware(request: Request) {
const response = NextResponse.next();
response.headers.set('Access-Control-Allow-Origin', '*');
return response;
}
export const config = { matcher: '/api/:path*' };
Using credentials: 'include' to send cookies? The Access-Control-Allow-Origin: * wildcard doesn't work — you must specify the exact origin:
// Client
fetch('/api/me', { credentials: 'include' });
// Server — '*' will NOT work with credentials
// 'Access-Control-Allow-Origin': '*' ❌
// 'Access-Control-Allow-Origin': 'http://localhost:3000' ✅
// Also required:
// 'Access-Control-Allow-Credentials': 'true'
Access-Control-Allow-OriginOPTIONS request and check its responseUse curl to test without browser CORS restrictions:
# Simulate a CORS preflight
curl -X OPTIONS https://api.example.com/data \
-H 'Origin: http://localhost:3000' \
-H 'Access-Control-Request-Method: GET' \
-v
Use HeoLab's HTTP Header Viewer to inspect the headers your server is actually returning, and HeoLab's Curl Converter to build curl commands from fetch code.