CORS
If your API is called from browser JavaScript on a different origin, you will meet CORS. It is one of the most misunderstood parts of web APIs, so let’s be precise about what it is and is not.
What CORS actually does
Section titled “What CORS actually does”The browser’s same-origin policy blocks a page on https://app.example.com from reading a response from https://api.example.com unless the API explicitly opts in with Cross-Origin Resource Sharing headers. CORS is enforced by the browser, on behalf of the user — it is not a server-side access control.
Simple requests and preflight
Section titled “Simple requests and preflight”For “non-simple” requests (most JSON POST/PUT/DELETE, custom headers), the browser first sends a preflight OPTIONS request asking permission:
sequenceDiagram participant B as Browser participant S as API B->>S: OPTIONS /articles (preflight) S-->>B: 204 + Access-Control-Allow-Origin/Methods/Headers B->>S: POST /articles (actual request) S-->>B: 201 Created + Access-Control-Allow-Origin
The key response headers: Access-Control-Allow-Origin (which origins may read responses), -Allow-Methods, -Allow-Headers, and -Allow-Credentials (whether cookies/credentials are allowed).
Configuring it
Section titled “Configuring it”Avoid the classic mistake of reflecting any origin (Access-Control-Allow-Origin: *) together with Allow-Credentials: true — the combination is forbidden and, more importantly, would expose authenticated responses to any site.
Common mistakes
Section titled “Common mistakes”Access-Control-Allow-Origin: * with credentials
Symptom:
- Setting
Access-Control-Allow-Origin: *while also sending cookies or anAuthorizationheader - The browser blocks the request —
*doesn’t work with credentials - Specify the real origin, e.g.
https://app.example.com, whenever credentials are used
A preflight on every request because nothing gets cached Symptom:
- No
Access-Control-Max-Ageset — the browser sends anOPTIONSpreflight every time - Extra latency on every request
- Set
Access-Control-Max-Age: 86400to cache the preflight result for a day
💡 Real-world examples
GitHub API:
- CORS allows only origins registered through an OAuth App
- No
*for authenticated endpointsStripe.js:
- The browser SDK calls the Stripe API directly — CORS is scoped to Stripe’s own domains
- The API key used in the browser is a publishable key only (no charge permission)
Sources: GitHub OAuth Apps, Stripe.js and Elements