Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Auth and the API Contract  ›  Lesson

CORS: When Client and Server Live Apart

RESTful APIs 14 minWhen a page calls an API on a different scheme, host or port, the browser only lets the page read the response if the server's CORS headers allow that origin, so apps either serve client and API from one origin or allow exact origins.

CORS: When Client and Server Live Apart

During development, Sprout's React app runs at http://localhost:5173 and its API at http://localhost:3000. The very first fetch fails with a red "blocked by CORS policy" message, even though the API answers perfectly when you call it from the terminal. Nothing is broken. The browser is enforcing a rule about origins, and once you know the rule the fix takes a minute.

The rule

Different origin: the browser asks, the server's headers answer.

An origin is three things together: scheme, host and port. http://localhost:5173 and http://localhost:3000 differ only in the port, and that is enough to make them different origins.

Under the browser's same-origin policy, a page's JavaScript may send a request to another origin, but it is not allowed to read the response unless that server says the page's origin is welcome. It says so with response headers:

Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true

For anything beyond a plain request, such as a JSON body or a PATCH or DELETE, the browser first sends a preflight: an OPTIONS request asking which methods and headers are allowed. Only if the answer permits it does the real request go out.

Cookies add two rules. The client must opt in with fetch(url, { credentials: 'include' }), and the server must name the exact origin and send Access-Control-Allow-Credentials: true. The wildcard Access-Control-Allow-Origin: * is refused when credentials are involved.

The most misunderstood point: CORS protects users' browsers, not your server. curl, scripts and tools such as Postman ignore it completely. It is never a replacement for login and ownership checks.

There are two good fixes.

First fix: make it one origin. In development, let Vite's dev server forward /api to the API, so the browser only ever talks to port 5173:

// client/vite.config.js
export default {
  server: {
    proxy: { '/api': 'http://localhost:3000' },
  },
};

In production, the web server does the same job: pages and /api share https://sprout.example.com, so CORS never comes up.

Second fix: allow a known origin. When the client really lives elsewhere, such as a page on app.sprout.example.com calling an API on api.sprout.example.com, or a partner's website, configure the API to allow an explicit list of exact origins.

The shortcut

When you see a CORS error, compare the page's origin with the request's origin: scheme, host, port. All three match? It is not CORS; look elsewhere. Any differ? Either proxy to make them match, or allow the page's origin on the server.

Sprout's first CORS errorStep 1 of 4
  1. The two origins

    The page is http://localhost:5173. The code calls fetch('http://localhost:3000/api/plants').

Drills

Answer fast — trust the rule.

quick drill

Which pair of URLs shares the same origin?

quick drill

Who enforces CORS?

quick drill

Does a strict CORS setup stop someone from calling Sprout's API with curl?

quick drill

Sprout sends cookies on cross-origin requests. Which server header does the browser refuse in that case?

Remember

  • Different origin: the browser asks, the server's headers answer.
  • Shortcut: compare scheme, host and port. Match means not CORS; mismatch means proxy or allow the origin.
  • Trap: treating CORS as security. It guards browsers only, and * does not work with cookies.

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainThe wildcard fix

A teammate 'fixed' Sprout's CORS error by adding Access-Control-Allow-Origin: * to every API response, saying: 'CORS isn't security anyway, so it doesn't matter.' Explain what is right and what is wrong in that reasoning, and what you would do instead.