You’ve built an API. It works perfectly in Postman. Then you call it from your frontend and get:
A h i c a s c s e p s b r s e e e s t n e o n b t f l e o o t c n c k h e t d h a e t b y r ' e h C q t O u t R e p S s s t : p e / d l a i r p c e i y s . : o e u x N r a o c m e p ' . l A e c . c c e o s m s ' - C f o r n o t m r o o l r - i A g l i l n o w ' - h O t r t i p g s i : n / ' m h y e a a p d p e . r c o m ' CORS isn’t your API being broken. It’s your browser protecting users. Understanding why it exists makes fixing it straightforward.
The Same-Origin Policy# Browsers enforce a security rule: JavaScript on one origin can’t freely access resources from another origin.
An origin is: protocol + host + port
h h h h t t t t t t t t p p p p s s : s : : : / / m / m a y m y p a y a i p a p . p p p m . p . y c . c a o c o p m o m p m . : c 8 o 0 m 8 0 → → → → O O O O r r r r i i i i g g g g i i i i n n n n A B C D ( ( ( d d d i i i f f f f f f e e e r r r e e e n n n t t t h p p o r o s o r t t t ) o ) c o l ) Without this restriction, malicious JavaScript on evil.com could make requests to yourbank.com using your cookies, stealing your data.
How CORS Works# CORS (Cross-Origin Resource Sharing) is the controlled exception to same-origin policy. Servers opt-in to allowing cross-origin requests by sending specific headers.
Simple Requests# For GET/POST with standard headers, the browser makes the request directly and checks the response:
1
2
3
GET /api/users HTTP / 1.1
Host : api.example.com
Origin : https://myapp.com
Server response:
1
2
3
4
5
HTTP / 1.1 200 OK
Access-Control-Allow-Origin : https://myapp.com
Content-Type : application/json
{ "users" : [ ... ]}
The Access-Control-Allow-Origin header tells the browser “yes, myapp.com is allowed to read this response.”
Preflight Requests# For “non-simple” requests (PUT, DELETE, custom headers, JSON content-type), the browser first sends an OPTIONS request:
1
2
3
4
5
OPTIONS /api/users/123 HTTP / 1.1
Host : api.example.com
Origin : https://myapp.com
Access-Control-Request-Method : DELETE
Access-Control-Request-Headers : Authorization, Content-Type
Server must respond with permissions:
1
2
3
4
5
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 : Authorization, Content-Type
Access-Control-Max-Age : 86400
Only then does the browser send the actual DELETE request.
Server-Side Configuration# Express.js# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const cors = require ( 'cors' );
// Allow specific origin
app . use ( cors ({
origin : 'https://myapp.com' ,
credentials : true
}));
// Allow multiple origins
app . use ( cors ({
origin : [ 'https://myapp.com' , 'https://staging.myapp.com' ],
credentials : true
}));
// Dynamic origin checking
app . use ( cors ({
origin : ( origin , callback ) => {
const allowed = [ 'https://myapp.com' , 'https://staging.myapp.com' ];
if ( ! origin || allowed . includes ( origin )) {
callback ( null , true );
} else {
callback ( new Error ( 'Not allowed by CORS' ));
}
}
}));
FastAPI (Python)# 1
2
3
4
5
6
7
8
9
from fastapi.middleware.cors import CORSMiddleware
app . add_middleware (
CORSMiddleware ,
allow_origins = [ "https://myapp.com" , "https://staging.myapp.com" ],
allow_credentials = True ,
allow_methods = [ "*" ],
allow_headers = [ "*" ],
)
Nginx# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always ;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always ;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type' always ;
add_header 'Access-Control-Allow-Credentials' 'true' always ;
if ( $request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400 ;
add_header 'Content-Length' 0 ;
return 204 ;
}
proxy_pass http://backend ;
}
Common Mistakes# Wildcard with Credentials# 1
2
3
# WRONG - browsers reject this combination
allow_origins = [ "*" ],
allow_credentials = True ,
You can’t use * (any origin) with credentials. The browser requires an explicit origin when cookies are involved. Specify exact origins instead.
Forgetting OPTIONS Handler# 1
2
3
4
# Only handles GET - preflight fails
@app.get ( "/api/data" )
def get_data ():
return { "data" : "value" }
Your framework’s CORS middleware usually handles OPTIONS automatically. If you’re manually adding headers, remember to handle OPTIONS requests.
www vs non-www# 1
2
# These are different origins!
allow_origins = [ "https://example.com" ] # Won't allow www.example.com
If users might access via both, allow both:
1
allow_origins = [ "https://example.com" , "https://www.example.com" ]
HTTP vs HTTPS# 1
2
# Protocol matters
allow_origins = [ "https://myapp.com" ] # Won't allow http://myapp.com
Debugging CORS Issues# Browser Console# The error message tells you exactly what’s wrong:
N → T → R → M → o h e e S e U q N t N ' e s u e h e A r v i e e o e c v a n s d d d c e l g t e r u t P t s e h o U o s i e T - s o w a a a C n f i d d i d o ' t e d s d n t t h r t h h n m r s e c f e o e o e r i a t t l n ' e e d h - d A d l e a o A i c e d r l d l n c n l l g e t X t o t o s i - o w o w C s a C e - O - l u A d A O R C s s c c r S o t c c i n o e e g h t m s s i e r - s s n a o H - - ' d l e C C e - a o o h r A d n n e s l e t t a l r r r d o o o e w i l l r - s - - O A A r n l l i o l l g t o o i w w n a - - ' l H M l e e h o a t e w d h a e e o d d r d e s s r m u s t n o t b e t h e w i l d c a r d w h e n c r e d e n t i a l s m o d e i s ' i n c l u d e '
Network Tab# Check the actual request/response headers:
Open DevTools → Network tab Find the failed request (might be OPTIONS) Check Response Headers for Access-Control-* headers Check if OPTIONS returns 200/204 or an error curl Testing# Test from command line (no CORS enforcement):
1
2
3
4
5
6
7
# Test the actual endpoint
curl -i https://api.example.com/data
# Simulate preflight
curl -i -X OPTIONS https://api.example.com/data \
-H "Origin: https://myapp.com" \
-H "Access-Control-Request-Method: POST"
When CORS Doesn’t Apply# CORS is browser-enforced. These bypass it entirely:
Server-to-server requests (no browser involved)Postman, curl, wget (not browsers)Mobile apps (native HTTP clients)Same-origin requests (no cross-origin)If it works in Postman but not in the browser, it’s definitely CORS.
Proxy as Alternative# If you can’t modify the API’s CORS headers, proxy through your own backend:
B r o w s e r → ( Y s o a u m r e B o a r c i k g e i n n d ) → ( E s x e t r e v r e n r a - l t o A - P s I e r v e r , n o C O R S )
1
2
3
4
5
// Your backend proxies the request
app . get ( '/api/proxy/weather' , async ( req , res ) => {
const response = await fetch ( 'https://external-api.com/weather' );
res . json ( await response . json ());
});
Now your frontend calls /api/proxy/weather — same origin, no CORS issues.
CORS errors aren’t bugs — they’re security features working correctly. The browser is asking “does this server want to allow this origin?” If the answer isn’t explicitly “yes,” the request fails.
Configure your allowed origins explicitly. Handle OPTIONS requests. Match protocols and subdomains exactly. The error messages tell you exactly what’s missing — read them carefully.