Skip to content

@tetsujs/cors

@tetsujs/cors lets a page on another origin call the API. It is one beforeParse hook: it answers the browser’s preflight request and adds the CORS headers to every other response.

Terminal window
bun add @tetsujs/cors
import { cors } from "@tetsujs/cors";
const browser = cors({
origin: string | readonly string[]

Which origins are allowed.

A list is matched exactly and echoed back — echoing rather than returning the list is what the header format requires. "*" allows every origin, and is refused together with credentials, which the specification does not permit.

An origin is written as a browser sends it — scheme, host and port, nothing after: https://app.example.com. Anything else never matches, so it is refused where it is written, naming the origin it means: a trailing slash, capitals, a path, a default port. "null" — what a sandboxed frame or a file: page sends — is taken as it is, and refused with credentials: any site can send it.

origin
: "https://app.example.com" });
createApp({ hooks: { beforeParse: [browser] }, routes });

On a request from an allowed origin, the hook puts its headers in ctx.out, which the core applies to whatever response leaves, errors and 404s included. A request from any other origin gets no CORS headers, and the browser keeps the answer from the page.

Mount it on the application, not on a group. Group hooks do not run for 404, 405 and OPTIONS preflights (see Groups and mounting), so CORS on a group would miss the preflight it is for.

Mount it before every hook that can refuse, such as an authentication check or a rate limit. A refusal from a hook placed before cors() goes out without CORS headers, and the browser cannot read it. A preflight also carries no credentials, so an authentication check placed first would refuse it.

createApp({ hooks: { beforeParse: [browser, auth, limit] }, routes });

Hooks that never refuse, such as requestId() and arrivalLog(), can go before it. Preflights then get a request id and an arrival line too; placed after it, they get neither.

Option Default
origin required "*", one origin, or a list of origins
methods GET, POST, PUT, PATCH, DELETE methods allowed in a preflight
headers content-type, authorization request headers a browser may send
exposeHeaders none response headers a browser may read
credentials false allow cookies and credentials; refused with origin: "*" and with "null"
maxAge 86400 how long a browser may cache a preflight, in seconds
const browser = cors({
origin: string | readonly string[]

Which origins are allowed.

A list is matched exactly and echoed back — echoing rather than returning the list is what the header format requires. "*" allows every origin, and is refused together with credentials, which the specification does not permit.

An origin is written as a browser sends it — scheme, host and port, nothing after: https://app.example.com. Anything else never matches, so it is refused where it is written, naming the origin it means: a trailing slash, capitals, a path, a default port. "null" — what a sandboxed frame or a file: page sends — is taken as it is, and refused with credentials: any site can send it.

origin
: ["https://app.example.com", "https://admin.example.com"],
credentials?: boolean | undefined

Whether credentials may be sent. Off by default.

credentials
: true,
exposeHeaders?: readonly string[] | undefined

Response headers a browser may read. Empty by default.

exposeHeaders
: ["x-request-id"],
});

Write each origin as a browser sends it: scheme, host and port, in lowercase, with nothing after. Origins are compared exactly, so https://app.example.com/ or https://App.example.com would never match. cors() throws at startup instead and names the origin you meant. Patterns are not supported: an origin containing * is refused.

Custom schemes such as capacitor://localhost or chrome-extension://… work as long as they are written in lowercase. "null", which sandboxed frames and file: pages send, is accepted, but not together with credentials, since any site can send it.

On a request from an allowed origin:

  • access-control-allow-origin: the request’s origin, or *;
  • access-control-allow-credentials: true, with credentials;
  • access-control-expose-headers, with exposeHeaders.

An OPTIONS request from an allowed origin is answered by the hook itself with 204, plus access-control-allow-methods, access-control-allow-headers and access-control-max-age. It does not reach later hooks or a handler.

Unless origin is "*", every response carries vary: origin, including those without CORS headers. Without it, a shared cache could serve one origin’s answer to another. A Vary header the handler set is kept and merged.

  • The package also exports the types CorsOptions and CorsHook. Type a hook with ReturnType<typeof cors> (which is CorsHook) rather than AnyHook, which erases the slot the hook belongs to.
  • Writing a hook package shows how a package like this one is built.