С любовью ко всем разработчикам на свете ❤️
@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. The shipped expo example uses it
because the Expo app and the API server live on different origins.
cors() is cut from the client bundle — its body and the imports it uses are
removed, so it never ships to the browser. .middleware(...) arguments are
server-only, and the compiler prunes the then-unused @point0/cors import with
them.
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())originorigin 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's Origin header back. With no Origin
header it falls back to *.false — never sets Access-Control-Allow-Origin; no cross-origin client
is allowed.'example.com' matches http://example.com and https://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 match notexample.com..test(requestOrigin); the full origin string (with protocol) is
tested.boolean (may be
async). Return true to allow the request's origin.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.
preflightA 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 headerscors({
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 the
Access-Control-Request-Method on a preflight). '*' and a single method
like 'GET' are also accepted; false omits the header.allowedHeaders: true — echoes the Access-Control-Request-Headers the
preflight asked for (falling back to the keys present on the request).exposeHeaders: true — exposes the keys present on the request.cors({
credentials: true, // Access-Control-Allow-Credentials: true (default)
maxAge: 600, // Access-Control-Max-Age in seconds
})credentials defaults to true, which sends
Access-Control-Allow-Credentials: true. Set it to false to drop the
header. With credentials on, an allowed origin is always echoed back literally
— the spec forbids * together with credentials, and cors() honors that.maxAge is the preflight cache lifetime in seconds, written to
Access-Control-Max-Age. It defaults to 5. A maxAge of 0 omits the
header entirely.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) |
| Aspect | Behavior |
|---|---|
| Side | cut from the client bundle — the .middleware(...) argument and its imports are removed, so it never ships to the browser |
| 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.