CORS
- Category: Extra
@point0/cors is a CORS middleware. cors(options) returns a Point0
middleware function: mount it on your root and every response
gets the Access-Control-* headers, and every preflight OPTIONS request gets
a 204 — so a client on a different origin (a native/mobile app, or a front-end
served from another domain) can call your API.
import { Point0 } from '@point0/core'
import { cors } from '@point0/cors'
// examples/expo/src/lib/root.tsx — let the native app reach the API
export const root = Point0.lets
.root()
.middleware(cors()) // ← every response carries CORS headers; OPTIONS → 204
// ...
.root()That bare cors() is the most permissive default: it reflects whatever
Origin the request carried, allows credentials, mirrors the request method,
and echoes the requested headers back. It's exactly what the shipped expo
example uses, because the Expo app and the API server live on different origins.
The rest of this page covers locking that down with origin, and the other
options.
cors() is cut from the client bundle — its body and the imports it uses are
removed, so it never ships to the browser. Middleware is server-only; the
package also has an explicit client branch that just calls next(), so even
where the call survives on the client it's a no-op.
Mounting it
cors(...) is a plain Point0 middleware, so it goes wherever
.middleware goes. On the root it covers the whole API, which
is the usual placement:
Point0.lets
.root()
.middleware(cors({ origin: 'https://app.example.com' }))
.root().middleware also takes a route, so you can scope CORS to a subtree:
.middleware('/api/*', cors())Locking down the origin: origin
origin decides which request origins are allowed. It defaults to true.
cors({ origin: true }) // allow any origin (default)
cors({ origin: false }) // allow none
cors({ origin: 'https://app.example.com' }) // one origin
cors({ origin: ['https://app.example.com', 'https://admin.example.com'] }) // a list
cors({ origin: /\.example\.com$/ }) // a RegExp tested against the origin
cors({ origin: (ctx) => isAllowed(ctx) }) // a predicateHow each form resolves:
true— reflects the request'sOriginheader back. With noOriginheader it falls back to*.false— never setsAccess-Control-Allow-Origin; no cross-origin client is allowed.- string — compared against the request origin. A bare host like
'example.com'matcheshttp://example.comandhttps://example.com(the protocol is ignored). Include a protocol —'https://example.com'— and the protocol must match too. The match is exact on the host, not a substring:'example.com'does not matchnotexample.com. - RegExp —
.test(requestOrigin); the full origin string (with protocol) is tested. - function — receives the middleware context and returns
boolean(may be async). Returntrueto allow the request's origin. - array — any mix of the above; the first entry that allows the origin wins.
When a request origin is allowed, that exact origin is echoed back in
Access-Control-Allow-Origin (not a literal *), which is what browsers
require once credentials are in play.
Preflight: preflight
A cross-origin request that isn't "simple" makes the browser send an OPTIONS
preflight first. By default (preflight: true) cors() answers it with a bare
204 carrying the CORS headers — the real request then follows. Set
preflight: false to let OPTIONS fall through to your own handler untouched
(no CORS headers added):
cors({ preflight: false }) // OPTIONS is passed through to next(), no CORS headersMethods and headers
cors({
methods: ['GET', 'POST'], // Access-Control-Allow-Methods
allowedHeaders: ['Content-Type', 'Authorization'], // Access-Control-Allow-Headers
exposeHeaders: ['X-Total-Count'], // Access-Control-Expose-Headers
})Each of methods, allowedHeaders, and exposeHeaders takes a string, a
string array (joined with , ), or a boolean. They default to true, which
means "reflect what the request asked for":
methods: true— mirrors the incoming request's method (or theAccess-Control-Request-Methodon a preflight).'*'and a single method like'GET'are also accepted;falseomits the header.allowedHeaders: true— echoes theAccess-Control-Request-Headersthe preflight asked for (falling back to the keys present on the request).exposeHeaders: true— exposes the keys present on the request.
Credentials and caching
cors({
credentials: true, // Access-Control-Allow-Credentials: true (default)
maxAge: 600, // Access-Control-Max-Age in seconds
})credentialsdefaults totrue, which sendsAccess-Control-Allow-Credentials: true. Set it tofalseto drop the header. Note that with credentials on, an allowed origin is always echoed back literally — the spec forbids*together with credentials, andcors()honors that.maxAgeis the preflight cache lifetime in seconds, written toAccess-Control-Max-Age. It defaults to5. AmaxAgeof0omits the header entirely.
Reference
cors(options)
Every option is optional; cors() with no argument is the permissive default.
| Option | Type | Default | What |
|---|---|---|---|
origin | boolean | string | RegExp | fn | array of those | true | which origins are allowed (true reflects the request origin) |
methods | boolean | '*' | method | method[] | true | Access-Control-Allow-Methods (true mirrors the request) |
allowedHeaders | true | string | string[] | true | Access-Control-Allow-Headers (true reflects the request) |
exposeHeaders | true | string | string[] | true | Access-Control-Expose-Headers (true reflects the request) |
credentials | boolean | true | send Access-Control-Allow-Credentials: true |
maxAge | number | 5 | Access-Control-Max-Age in seconds (0 omits the header) |
preflight | boolean | true | answer OPTIONS preflight with 204 (else pass through) |
Behavior at a glance
| Aspect | Behavior |
|---|---|
| Side | cut from the client bundle — body and its imports removed, never ships to the browser (server-only; a no-op on the client via next()) |
| Default origin | reflects the request Origin; * only when there's no Origin header |
| Credentials + origin | an allowed origin is echoed literally, never * (spec requirement) |
| String origin match | host-only by default; add a protocol to require a protocol match |
Preflight OPTIONS | answered with 204 when preflight: true; passed through otherwise |
Vary | set to Origin for a reflected origin, * for a wildcard |
If you also expose OpenAPI docs from a separate origin, the same
cors() on the root covers those routes too.
Enjoying Point0?
Star Point0
Start0
YouTube
Discord
Telegram
of the Lord Jesus Christ ☦️
With love for developers
of all backgrounds around the world ❤️