Cross-Origin Resource Sharing (CORS) lets a server tell browsers which other origins may call it. By default a browser stops a page's script from reading another origin's responses, and CORS headers relax that rule in a controlled way.
An origin is the scheme, host and port together, so https://app.example.com and https://api.example.com are different origins. The server declares what it allows.
For a request that isn't simple, such as a PUT or one with an Authorization header, the browser first sends a preflightOPTIONS request. Access-Control-Max-Age lets the browser cache that answer.
Handle it with a CORS library rather than by hand.
Be specific with allowed origins, and don't use * for requests with credentials.
Validate the Origin header against an allowlist on the server.
CORS only guides browsers. Other clients ignore it, so it never replaces authentication.
Rewriting in plainer words…
This answer doesn't lend itself to a diagram - it reads best . No credits were charged.
The browser runs the same-origin policy to protect its users. And CORS headers tell the browser which origins may read the reply, but requests from curl, POSTMAN don't go through that check.
For this question · all 2 chaptersThe Access-Control headers in this answer are read by the browser's gate, never by your server. Watch the terminal's copy of the same GET land in full while the page's copy waits at the gate, then the preflight chapter: two lists the browser checks before a PUT ever leaves.
1/2CORS is browser's check
CORS is browser's check
Your call to the endpoint works from Postman, but the same call from your page fails with "blocked by CORS policy". Nothing is wrong with the server. An origin is three parts: scheme, host and port. Here app.example.com and api.example.com differ only in the host, so the browser blocks your page.
This first request is a plain GET /orders. The browser adds an Origin header, which specify the page that made the call. The server answers 200 and sends the body back. The reply reaches the browser, but the browser refuses to hand that response to the page.
The server writes the Access-Control-Allow-Origin header, and the browser reads that header as a pass. The header speicfy one allowed origin, https://app.example.com. The browser lets the response reach the page because the server specified that origin. The allow list comes from the server, but the browser checks the list and decides.
A PUT with a JSON body and an Authorization header is not a simple request. So the browser asks the server first, with an OPTIONS preflight carrying Access-Control-Request-Method and Access-Control-Request-Headers. The server answers with Allow-Methods, Allow-Headers and Max-Age. Two things trigger that preflight: any method other than GET, HEAD or POST, and any header the browser does not already allow.
The browser now sends the PUT request itself. The server answers with 200, and the response passes the browser's check. The second PUT sends no preflight OPTIONS request, because Max-Age told the browser to remember the answer for 600 seconds, per URL and per method. Your dev tools then show fewer OPTIONS calls than you expected.
A terminal sends the same GET /orders request, and no browser is involved. Nothing enforces CORS here, so the full response body comes back. The CORS check never runs for curl, Postman, a cron job or another server, so CORS does not protect the API. Authentication protects the API, and CORS decides if a page inside a browser may read the response.
The browser refuses a wildcard when credentials are involved. So many servers copy back whatever Origin the request sent, and add Allow-Credentials: true. A tab at evil.example sends GET /orders request, and the browser attaches the session cookie if that cookie is allowed cross-site. The browser's check reads the copied Origin and allows the read, so the untrusted page gets the user's orders.
Allow one named origin again, so the browser blocks the evil tab from reading the reply. The terminal still receives the reply, as nothing about the terminal changed. Keep a real list of allowed origins, and return the single origin that matched. Send Vary: Origin as well, so a cache never gives one origin's permission to another.
Preflight is two lists that must overlap
The browser tab sends three requests, and only the PUT sends a preflight. The preflight carries a question, and the reply carries the answer. GET, HEAD and a form POST with no custom header are simple requests, so the browser sends them without a preflight. The PUT carries JSON and an Authorization header, so the browser asks first.
The browser states exactly what it is about to send. Access-Control-Request-Method says PUT, and Access-Control-Request-Headers lists authorization and content-type. The preflight request carries those two lines ahead of the real call. Dev tools may show an OPTIONS request you didn't wrote, that one is from the browser.
The server answers the preflight with two lists. Allow-Methods specify GET and PUT. Allow-Headers has authorization and content-type. The planned PUT and its headers are all on those lists, so the browser sends the real PUT request.
The page now sends a custom header, X-Trace-Id, so the browser sends the preflight OPTIONS request again. The browser asks about x-trace-id, and the server's allowed list has no entry with that name. The browser then refuses to send the PUT, so the server log shows one OPTIONS and no PUT. The fix is to add x-trace-id to Access-Control-Allow-Headers.
The server now lists x-trace-id, and the page sends its session cookie with the PUT. The server respond with Allow-Origin: *, and the browser rejects that star, because CORS does not allow a wildcard with a credential. The server must specify the origin, https://app.example.com, and add Allow-Credentials: true, so the page may read the response. The wildcard rule is why every credentialed API sends back a matched origin instead of a star.
The browser reads Max-Age: 600 and keeps the preflight answer for ten minutes, one answer per URL and method. So the next PUT skips its preflight request and saves a round trip. Chromium caps that cache at two hours, Firefox at 24 hours. The page can read x-request-id in the reply only because the server listed that name under Access-Control-Expose-Headers.
Chapter 1 · step 1 of 8
Your call to the endpoint works from Postman, but the same call from your page fails with "blocked by CORS policy". Nothing is wrong with the server. An origin is three parts: scheme, host and port. Here app.example.com and api.example.com differ only in the host, so the browser blocks your page.
So, CORS is the browser protecting its user. The browser can ask permission before it makes the real call for some requests - the preflight. A blocked preflight ends the exchange right there. And in this case, the browser don't send the actual request at all.
Saved in this browser - sign in to keep your review list.
How should your speech become text?
Listening… your words appear above as you speak - tap Stop when you're done.
Recording · cr - tap Stop & transcribe when you're done.
Transcribing with AI…
Voice:
Keep going - a few more words and AI can grade it.
Interview lens
How interviewers actually use this question
Likely follow-ups
Your API uses session cookies. What changes in the CORS setup so cross-origin calls work?Allow-Credentials: true, an exact origin instead of *, the client opting in to send credentials, and Vary: Origin when you echo the origin.
Does CORS protect your API from CSRF? Why or why not?Simple cross-origin requests still reach the server with cookies, and CORS only blocks reading the response. Name CSRF tokens or SameSite cookies instead.
Which requests skip the preflight, and why does a JSON POST still trigger one?Simple means GET, HEAD or POST with safelisted headers and a form or text/plain content type. application/json is not on that list.
The preflight succeeds but the browser still blocks the real response. Where do you look?The actual response needs Allow-Origin too. Check error paths, proxies or CDNs that drop the headers, and cached responses without Vary: Origin.
An API gateway sits in front of your services. Where should the CORS headers be set?Set them in one layer so responses don't carry duplicate or conflicting headers, and make sure auth checks don't reject the OPTIONS preflight.
What you can say
CORS is how a server tells browsers which other origins may read its responses, relaxing the browser's default same-origin rule in a controlled way.
An origin is the scheme, host and port together, so app.example.com and api.example.com count as different origins.
The server declares what it allows with headers like Access-Control-Allow-Origin, Allow-Methods and Allow-Headers.
For a request that isn't simple, like a PUT or one with an Authorization header, the browser sends a preflight OPTIONS request first, and Max-Age lets it cache that answer.
In practice I use a CORS library, check the Origin header against an allowlist, and never use * when the request carries credentials.
CORS only guides browsers, and curl or a backend service ignores it, so it never replaces authentication.
Weak answers to avoid
Treats CORS as server-side securityCORS only binds browsers. Curl, scripts and other servers skip it, so say plainly that it never replaces authentication or authorization.
Reaches for * or echoes back any OriginA wildcard fails for requests with cookies, and reflecting any Origin with credentials lets any site read user data. Check Origin against an allowlist.
Thinks CORS stops the request reaching the serverSimple requests still hit the server and run. The browser only hides the response from the page's script, which is why CORS is no CSRF defense.
Can't say when a preflight happensSaying the browser sends OPTIONS isn't enough. Name what triggers it, such as a PUT or an Authorization header, and how Max-Age cuts repeat preflights.
Interview lens
Likely follow-ups, what you can say, and the weak answers to avoid.