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.
| Rule | Default |
|---|---|
allowOrigin | "*" |
allowMethods | GET, POST, PUT, PATCH, DELETE, OPTIONS |
allowHeaders | "*" |
maxAge | "86400" (one day) |
allowCredentials | false |
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:
- Parent router’s rules
- Mounted router’s rules
- The route’s own
cors, if set viaaddRoute({ 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:
- Read
Access-Control-Request-Methodand resolve that method’s route - 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
allowHeadersif 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.