CORS: Cross-Origin Requests and Preflight

Intermediate
12 min

CORS: Cross-Origin Requests and Preflight

Your API works perfectly in Postman, then the browser refuses to call it with a red "blocked by CORS policy" message. In this lesson you will learn what the same-origin policy protects, how the browser negotiates cross-origin access, which response headers control it, and how to configure an Express API correctly.

The Same-Origin Policy

An origin is the combination of scheme, host and port. Browsers let a page freely read responses only from its own origin; anything else is cross-origin:

| Page origin | Request URL | Same origin? | |---|---|---| | https://app.example.com | https://app.example.com/api/books | Yes | | https://app.example.com | https://api.example.com/books | No (different host) | | http://localhost:5173 | http://localhost:3000/books | No (different port) |

The policy stops a malicious site from using your logged-in browser to read data from your bank's API. Cross-Origin Resource Sharing (CORS) is how a server tells the browser "this other origin may read my responses". Browsers alone enforce it; curl, Postman and server-to-server calls never see it.

Simple Requests and Preflight

For a simple request (GET, HEAD or POST with only basic headers and a form-style or text/plain content type) the browser sends the request immediately with an Origin header and checks the response afterwards. Anything else, including JSON bodies and an Authorization header, triggers a preflight: an OPTIONS request asking for permission first.

bash
# Preflight sent automatically by the browser OPTIONS /books HTTP/1.1 Origin: https://app.example.com Access-Control-Request-Method: POST Access-Control-Request-Headers: content-type, authorization # Server approval HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PATCH, DELETE Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Max-Age: 600

Only if the preflight succeeds does the real request go out. A failed check on a simple request does not stop it reaching the server; it only stops the page from reading the response.

The CORS Response Headers

| Header | Purpose | |---|---| | Access-Control-Allow-Origin | Origin allowed to read the response, or * | | Access-Control-Allow-Methods | Methods permitted in the real request | | Access-Control-Allow-Headers | Extra request headers permitted | | Access-Control-Expose-Headers | Response headers JavaScript may read | | Access-Control-Allow-Credentials | true allows cookies; forbids * as origin | | Access-Control-Max-Age | Seconds to cache the preflight result |

When you echo a specific origin instead of *, add Vary: Origin so caches do not serve one origin's approval to another.

Configuring Express

The cors package sets all of these headers and answers preflights for you:

javascript
const cors = require("cors"); const allowedOrigins = ["https://app.example.com", "http://localhost:5173"]; app.use( cors({ origin: allowedOrigins, methods: ["GET", "POST", "PATCH", "DELETE"], allowedHeaders: ["Content-Type", "Authorization"], exposedHeaders: ["X-Total-Count", "Location"], credentials: true, maxAge: 600, }) );

origin accepts a string, an array, a regular expression or a function, so preview deployments can be allowed with a pattern. Register the middleware before your routes; a preflight that reaches a 404 handler fails.

Credentials and Cookies

Cross-origin requests do not send cookies unless the client asks and the server answers with Access-Control-Allow-Credentials: true plus an explicit origin:

javascript
const res = await fetch("https://api.example.com/me", { credentials: "include" });

Bearer tokens in an Authorization header do not need this flag; the header only has to be listed in Access-Control-Allow-Headers.

Common Mistakes

  • Using origin: "*" in production. It lets any website call your API and cannot be combined with credentials.
  • Forgetting exposedHeaders. Custom headers such as X-Total-Count are invisible to JavaScript unless exposed.
Quick Quiz
Question 1 of 2

A page at `http://localhost:5173` calls `http://localhost:3000/books`. Why is this cross-origin?

Key Takeaways

  • An origin is scheme + host + port; browsers block reading cross-origin responses unless CORS allows it.
  • Non-simple requests (JSON bodies, Authorization headers) trigger an OPTIONS preflight.
  • The server opts in with Access-Control-Allow-* headers; only the browser enforces them.
  • In Express, app.use(cors({...})) with an explicit origin list handles headers and preflights.
  • CORS is not authentication; never rely on it to protect data.

Next lesson: API Authentication with API Keys — issue, validate and rotate keys for programmatic access.

CORS: Cross-Origin Requests and Preflight - REST APIs | CodeYourCraft | CodeYourCraft