Your API is unusable from a front-end on another origin until CORS is set up — and dangerous if set up carelessly.
Browsers enforce the same-origin policy: JavaScript on https://app.example.com may send a request to https://api.example.com, but cannot read the response unless the API says so with CORS headers. CORS is a browser rule; curl, servers and mobile apps ignore it. It is not authentication — it controls which websites' scripts may read your responses.
The mechanics:
- For simple requests (GET/POST with basic headers), the browser sends the request with an
Originheader and checks the response'sAccess-Control-Allow-Origin. - For anything else —
PUT/DELETE,Content-Type: application/json, anAuthorizationheader — the browser first sends a preflightOPTIONSrequest asking permission (Access-Control-Request-Method,-Headers). Your server must answer it with the allowed methods and headers. - To send cookies, the client sets
credentials: "include"and the server must returnAccess-Control-Allow-Credentials: truewith a specific origin —*is not allowed with credentials.
The cors package handles all of this:
cors({ origin: ["https://app.example.com"] })— allow a fixed list (it echoes the matching origin and setsVary: Origin).origincan also be a function for dynamic checks, e.g. per-tenant domains.credentials: true,methods,allowedHeaders,exposedHeaders(to let JS read custom response headers),maxAge(cache the preflight).
Mount it before your routes so preflights get answered. Avoid reflecting any origin with credentials — that lets every website make authenticated requests on your users' behalf. If front-end and API share an origin (via a reverse proxy), you don't need CORS at all.
Example
import express from "express";
import cors from "cors";
const app = express();
const allowed = new Set(["https://app.example.com", "http://localhost:5173"]);
app.use(cors({
origin(origin, callback) {
// Non-browser clients send no Origin header; allow them (CORS doesn't apply)
if (!origin || allowed.has(origin)) return callback(null, true);
callback(null, false); // no CORS headers -> browser blocks the read
},
credentials: true, // allow cookies; requires a specific origin
methods: ["GET", "POST", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
maxAge: 600, // cache preflight for 10 minutes
}));
app.use(express.json());
app.patch("/api/profile", (req, res) => res.json({ ok: true }));
app.listen(3000);
// Preflight: curl -i -X OPTIONS localhost:3000/api/profile \
// -H "Origin: http://localhost:5173" -H "Access-Control-Request-Method: PATCH"Remember
- CORS lets chosen origins' scripts read your responses; it isn't auth
- Non-simple requests trigger an OPTIONS preflight you must answer
- Credentials require a specific origin, never
* - Mount
cors()before routes; allow-list origins
Watch out for
- Using
origin: true(reflect any origin) together withcredentials: true.