Skip to content
Express.js ex libraries 5 min read

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 or Authorization headers 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.

OptionTypePurpose
originstring / array / RegExp / functionWhich origins are allowed
methodsstring / arrayAllowed HTTP methods (default all common verbs)
allowedHeadersstring / arrayRequest headers the client may send
exposedHeadersstring / arrayResponse headers JS is allowed to read
credentialsbooleanSend Access-Control-Allow-Credentials: true
maxAgenumberSeconds the browser may cache the preflight
optionsSuccessStatusnumberStatus 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, or Authorization headers — allowlist explicit origins instead.
  • Pair credentials: true with a specific reflected origin; the browser rejects credentials sent alongside Access-Control-Allow-Origin: *.
  • Drive your allowed-origins list from environment variables so staging and production differ without code changes.
  • Set a sensible maxAge to let browsers cache preflights and cut redundant OPTIONS round trips.
  • In Express 5, replace app.options("*", ...) with a named wildcard like app.options("/{*splat}", ...); the old bare * no longer parses.
  • Mount cors before your routes and before authentication middleware so preflight OPTIONS requests are answered without hitting auth checks.
  • Use exposedHeaders whenever client code must read custom response headers such as pagination counts — otherwise the browser hides them.
Last updated June 14, 2026
Was this helpful?