CORS

CORS headers on matched routes and OPTIONS preflight - no manual CORS middleware required for common cases.

Defaults

HttpServer ships with permissive defaults. Override them with .cors() on the server or any mounted Router.

RuleDefault
allowOrigin"*"
allowMethodsGET, POST, PUT, PATCH, DELETE, OPTIONS
allowHeaders"*"
maxAge"86400" (one day)
allowCredentialsfalse

Merging rules

.cors(rules) merges into the router’s current rules (later keys override earlier ones). When a router is mounted, each route stores a merged cors field:

  1. Parent router’s rules
  2. Mounted router’s rules
  3. The route’s own cors, if set via addRoute({ cors })
import { HttpServer, Router } from "jsr:@webtools/expressapi";

const api = new Router("/v1")
  .cors({ allowOrigin: "https://app.example.com", maxAge: "7200" })
  .get("/health", (_req, res) => res.json({ ok: true }));

const server = new HttpServer()
  .cors({ allowOrigin: "*" })
  .use(api);

Preflight

For OPTIONS, the server responds 204 with CORS headers and no body. To pick which route’s rules apply when several methods share a path:

  1. Read Access-Control-Request-Method and resolve that method’s route
  2. If the header is missing or unknown, fall back to the first matching path among GET, POST, PUT, PATCH, DELETE

Dynamic values

allowOrigin, allowMethods, and allowHeaders accept a string or (req) => string | undefined (possibly async). When the resolved origin is not *, a Vary: Origin header is added automatically.

import type { CorsRules } from "jsr:@webtools/expressapi";

const rules: CorsRules = {
  allowOrigin: (req) => req.headers.get("origin") ?? undefined,
  allowCredentials: true,
};

Credentials

Browsers reject Access-Control-Allow-Origin: * when credentials (cookies, Authorization with credentialed fetch) are involved. Reflect a concrete origin instead:

const server = new HttpServer().cors({
  allowOrigin: (req) => req.headers.get("origin") ?? undefined,
  allowCredentials: true,
  allowHeaders: "Content-Type, Authorization",
});

Origin whitelist

Return the request origin only when it is allowed; returning undefined skips theAccess-Control-Allow-Origin header.

const allowed = new Set([
  "https://app.example.com",
  "http://localhost:5173",
]);

const server = new HttpServer().cors({
  allowOrigin: (req) => {
    const origin = req.headers.get("origin");
    return origin && allowed.has(origin) ? origin : undefined;
  },
  allowCredentials: true,
});

Restricting headers and methods

Defaults allow all request headers (*). If you set a concrete allowHeaders list, every custom header the browser sends on preflight must appear in it (for example Authorization, X-Request-Id).

server.cors({
  allowMethods: "GET, POST, OPTIONS",
  allowHeaders: "Content-Type, Authorization",
  maxAge: "3600",
});

SPA checklist

  • Front and API on different origins → configure CORS on the API
  • Credentialed fetch(..., { credentials: "include" }) → concrete origin + allowCredentials: true
  • Custom headers → list them in allowHeaders if you left the default *
  • Preflight is handled; do not add a manual OPTIONS route unless you need a custom body

See the full walkthrough in CORS for a SPA.