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.
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.
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.
# 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: 600Only 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.
| 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.
The cors package sets all of these headers and answers preflights for you:
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.
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:
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.
origin: "*" in production. It lets any website call your API and cannot be combined with credentials.exposedHeaders. Custom headers such as X-Total-Count are invisible to JavaScript unless exposed.A page at `http://localhost:5173` calls `http://localhost:3000/books`. Why is this cross-origin?
Authorization headers) trigger an OPTIONS preflight.Access-Control-Allow-* headers; only the browser enforces them.app.use(cors({...})) with an explicit origin list handles headers and preflights.Next lesson: API Authentication with API Keys — issue, validate and rotate keys for programmatic access.