cors Middleware
Browsers enforce the same-origin policy, which blocks JavaScript on https://app.example.com from reading a response served by https://api.example.com unless that API explicitly opts in. Cross-Origin Resource Sharing (CORS) is the set of HTTP headers that grant that permission, and the cors package is the official Express middleware for emitting them correctly. It handles the tricky parts — preflight OPTIONS requests, credentialed requests, and origin allowlisting — so you do not have to set Access-Control-* headers by hand.
Installing and enabling cors
Install the package and mount it as middleware. Calling cors() with no arguments is the most permissive configuration: it reflects any origin with Access-Control-Allow-Origin: *, which is fine for a fully public API but too open for anything that uses cookies or auth.
npm install cors
const express = require("express");
const cors = require("cors");
const app = express();
app.use(cors()); // allow all origins
app.use(express.json());
app.get("/api/ping", (req, res) => {
res.json({ ok: true });
});
app.listen(3000, () => console.log("Listening on http://localhost:3000"));
A request from a browser app on another origin now receives the permissive header:
Output:
< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: *
< Content-Type: application/json
{"ok":true}
The wildcard
*and credentials are mutually exclusive. If you need to send cookies orAuthorizationheaders cross-origin, you must reflect a specific origin instead of*, or the browser will reject the response.
Allowlisting specific origins
In production you almost always want to restrict which origins may call your API. Pass an origin option — a string, an array of strings, a regular expression, or a function for dynamic checks. A function receives the request’s Origin header and a callback, letting you validate against a database or environment-driven list.
const allowedOrigins = [
"https://app.example.com",
"https://admin.example.com",
];
app.use(
cors({
origin(origin, callback) {
// requests with no Origin (curl, server-to-server) are allowed
if (!origin || allowedOrigins.includes(origin)) {
return callback(null, true);
}
callback(new Error(`Origin ${origin} not allowed by CORS`));
},
})
);
When the origin matches, cors reflects it back exactly rather than using *:
Output:
< Access-Control-Allow-Origin: https://app.example.com
< Vary: Origin
The Vary: Origin header is added automatically so caches do not serve one origin’s allow header to another.
Methods, headers, and credentials
Beyond the origin, the cors options object controls which HTTP methods, request headers, and credential behavior the browser is permitted to use. The table below covers the options you will reach for most often.
| Option | Type | Purpose |
|---|---|---|
origin | string / array / RegExp / function | Which origins are allowed |
methods | string / array | Allowed HTTP methods (default all common verbs) |
allowedHeaders | string / array | Request headers the client may send |
exposedHeaders | string / array | Response headers JS is allowed to read |
credentials | boolean | Send Access-Control-Allow-Credentials: true |
maxAge | number | Seconds the browser may cache the preflight |
optionsSuccessStatus | number | Status for preflight (use 200 for legacy clients) |
app.use(
cors({
origin: "https://app.example.com",
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
exposedHeaders: ["X-Total-Count"],
credentials: true, // required to read/write cookies cross-origin
maxAge: 86400, // cache preflight for 24 hours
})
);
With credentials: true, the browser’s fetch must also opt in on its side:
fetch("https://api.example.com/api/me", {
credentials: "include",
});
Preflight requests
For any “non-simple” request — a custom header, or a method like PUT or DELETE — the browser first sends an OPTIONS preflight to ask permission. The cors middleware answers these automatically when mounted with app.use. In Express 4 you could also register an explicit handler with app.options("*", cors()); note that in Express 5 the bare * wildcard string is no longer valid and you must use a named pattern such as app.options("/{*splat}", cors()).
// Express 5: handle preflight for every route explicitly (optional)
app.options("/{*splat}", cors());
A preflight exchange looks like this:
Output:
> OPTIONS /api/items HTTP/1.1
> Origin: https://app.example.com
> Access-Control-Request-Method: PUT
> Access-Control-Request-Headers: content-type
< HTTP/1.1 204 No Content
< Access-Control-Allow-Origin: https://app.example.com
< Access-Control-Allow-Methods: GET,POST,PUT,DELETE
< Access-Control-Allow-Headers: content-type
< Access-Control-Max-Age: 86400
Per-route CORS configuration
You do not have to apply one global policy. Because cors() returns ordinary middleware, you can pass a different configuration to individual routes or routers — for example, a public read endpoint open to everyone alongside a private write endpoint locked to your own front end.
const express = require("express");
const cors = require("cors");
const router = express.Router();
const publicCors = cors({ origin: "*" });
const privateCors = cors({ origin: "https://app.example.com", credentials: true });
// Open to any site
router.get("/public/feed", publicCors, async (req, res) => {
res.json({ items: await loadFeed() });
});
// Restricted, preflight handled per-route
router.options("/private/orders", privateCors);
router.post("/private/orders", privateCors, async (req, res) => {
const order = await createOrder(req.body);
res.status(201).json(order);
});
module.exports = router;
This per-route style keeps each endpoint’s policy close to its handler and avoids loosening the global rule for the sake of one path.
Best Practices
- Never ship
cors()(wildcard) for any endpoint that uses cookies, sessions, orAuthorizationheaders — allowlist explicit origins instead. - Pair
credentials: truewith a specific reflected origin; the browser rejects credentials sent alongsideAccess-Control-Allow-Origin: *. - Drive your allowed-origins list from environment variables so staging and production differ without code changes.
- Set a sensible
maxAgeto let browsers cache preflights and cut redundantOPTIONSround trips. - In Express 5, replace
app.options("*", ...)with a named wildcard likeapp.options("/{*splat}", ...); the old bare*no longer parses. - Mount
corsbefore your routes and before authentication middleware so preflightOPTIONSrequests are answered without hitting auth checks. - Use
exposedHeaderswhenever client code must read custom response headers such as pagination counts — otherwise the browser hides them.