skip to content

Your single-page app is served from one origin and its API from another, and calls carry the user's session. How would you decide between keeping that cross-origin arrangement with CORS and serving the API from the app's own origin through a reverse proxy path such as /api?

level: principalimportance: should knowfreq 34%

answer

  1. ask who else calls this API
  2. same origin deletes the whole class
  3. cross-site cookies are a shrinking bet
  4. the proxy is a hop you operate
  5. never wildcard a credentialed endpoint

basics

~20 s

Same-origin proxying removes CORS, advance permission checks and cross-site cookie exposure entirely, at the cost of an extra hop you must operate. Cross-origin with CORS is right when many independent or third-party clients need the API; a first-party SPA with a session usually belongs on one origin.

solid answer

~50 s

I frame it as: who else calls this API? If the SPA is effectively the only first-party consumer, putting it behind the app's own origin is the stronger default — no permission checks, no extra round trip on non-simple requests, a first-party cookie that browser cross-site restrictions cannot drop, and one fewer configuration surface where a permissive origin list can go wrong. The costs are real: another hop to operate and observe, streaming and upload behaviour to verify through it, and a shared origin means an XSS anywhere in the app already sits next to the API. Cross-origin with CORS earns its place when the API is a genuine product surface with many callers, when the frontend is deployed independently at many origins, or when the auth is a bearer token rather than a cookie so there is no cross-site cookie exposure at all. What I refuse either way is a permissive allow-list: origins get enumerated, preview deployments get a deliberate mechanism, and reflection is always gated.

go deeper

for a junior

Know the two shapes — calling an API on another origin, or routing it under your own app's path — and that the second one means the browser applies no cross-origin checks at all.

for a middle

Explain the concrete differences: the extra round trip on non-simple requests, and a first-party cookie versus a cross-site one that must be marked SameSite=None and can be dropped by browser restrictions.

for a senior

Argue the tradeoff with operational detail — what the proxy hop costs to run, how streaming and uploads behave through it, and why a dev-server proxy that hides cross-origin behaviour until staging is a trap.

for a principal

Own the boundary decision and the guardrails: who the API's callers really are, how preview origins are admitted without a permissive rule, and the standing position that CORS is never an authorization control.

## Frame the decision, do not default This is a boundary question, not a CORS-configuration question. The real inputs are: how many independent clients the API has, what carries authentication, who operates the edge, and how many origins the frontend is deployed to. CORS is a mechanism for letting *other people's* origins read your responses. Using it to let your own app talk to your own API is legal but is often paying a security and latency price for a separation you did not need. ## What same-origin proxying buys Route `https://app.example.com/api/*` at the edge to the upstream service and the browser sees same-origin requests. That erases several categories of problem at once: - **No permission checks, ever.** Requests with JSON bodies, `Authorization` headers, `PATCH` and `DELETE` all go straight out. On a chatty API over a high-latency mobile link, removing an extra round trip per non-simple request is a measurable win that no CORS tuning can match. - **First-party cookies.** A same-origin session cookie is not a cross-site cookie: it needs no `SameSite=None`, it is not exposed to third-party cookie restrictions, and it does not sit in the category browsers are actively tightening. You stop betting your login flow on a moving target. - **One less permissive surface.** No allow-list of origins to maintain, no risk that a reflection rule ships without its check, no divergence between environments. - **A simpler failure vocabulary.** Failures become statuses your code can read instead of console messages your code cannot. ## What it costs - **A hop you own.** The proxy is now in the request path for every API call: it needs capacity, timeouts, health checks, tracing and a deploy story. If the frontend is served from a static host or a CDN with limited rewrite capability, this may be a real constraint rather than a config line. - **Behaviour to verify.** Streaming responses, server-sent event connections, large uploads, and long-lived requests all behave differently through an intermediary that buffers. Test them explicitly rather than assuming transparency. - **A merged blast radius.** Same origin means the API and the app share cookies, storage and script context. A cross-site scripting flaw anywhere in the app is already same-origin with the API — although in practice a cross-origin arrangement with a credentialed allow-list is barely better, since the compromised page is on the list. - **A local-development illusion.** A dev-server proxy makes everything same-origin locally, which silently hides cross-origin problems until staging. If production is cross-origin, development should be too. ## When cross-origin with CORS is the right answer - **The API is a product.** Third-party developers, partner integrations, or public documentation mean the API must serve origins you do not control. That is exactly what CORS is for. - **Many first-party origins.** Several apps, white-label domains, or per-customer subdomains that all call one API. Proxying each one duplicates the edge; an allow-list centralises the decision. - **Token-based auth.** If callers send a bearer token in an `Authorization` header rather than relying on a cookie, nothing ambient is attached, credentialed-mode rules never engage, and the cross-site cookie question disappears. The remaining cost is one advance check per endpoint, amortised by the browser remembering the approval. - **Organisational reality.** If the API team and the frontend team ship independently and no one owns a shared edge, an explicit CORS contract may be the honest interface. ## The rules I hold either way 1. **Enumerate origins; never wildcard a credentialed endpoint.** If origins are computed, the computation is an allow-list lookup, not an echo of whatever arrived. 2. **Give preview deployments a deliberate mechanism.** Per-branch URLs are the usual reason a permissive pattern gets shipped. Either match them against a strict pattern in non-production only, or route previews through the same proxy the production app uses. 3. **Never treat CORS as authorization.** Every endpoint authenticates and authorizes on its own, because a non-browser client ignores CORS entirely and a cross-origin write can reach the server regardless. 4. **Make the choice reversible and observable.** Whichever way it goes, know how you would detect that browsers stopped attaching a cross-site cookie, or that the proxy started buffering a stream — the failure modes are quiet on both paths. ## How I would say it in one line A first-party SPA with a cookie session usually belongs on the same origin as its API; CORS is the right tool when the set of callers is genuinely open, and in that case the auth should not depend on ambient cookies.

  • What would make you keep the cross-origin arrangement even for a single first-party SPA?
    No team owning a shared edge, a static host that cannot rewrite paths, or a frontend deployed to many domains where proxying multiplies infrastructure. Token-based auth also weakens the argument for merging, since the cross-site cookie exposure that motivates it disappears. In those cases I would keep CORS with an enumerated allow-list and accept the extra round trip on non-simple requests.
  • Preview deployments get a fresh subdomain per branch. How do you support them without a permissive allow-list?
    Keep production's list strictly enumerated and handle previews separately: match preview origins against a tight pattern that is only accepted by non-production environments, or route previews through the same proxy path the production app uses so they are same-origin too. What I avoid is a pattern loose enough to match an attacker-registered host, and any rule that reaches production.
  • If you consolidate onto one origin, what have you not improved?
    Authorization, and the blast radius of cross-site scripting. Same-origin means the app's scripts already sit inside the API's origin, so a script injection is at least as damaging as before. Every endpoint still needs its own authentication and per-object authorization, and any anti-forgery expectations have to be re-examined since the request is no longer cross-site.

saying these in an interview costs you the question

  • Says a proxy fixes CORS without mentioning the operating cost
  • Believes same-origin removes the need for authorization
  • Keeps a wildcard allow-list because the API is internal
  • Runs same-origin locally while production stays cross-origin
  • Assumes cross-site session cookies will keep working indefinitely

context