-
Notifications
You must be signed in to change notification settings - Fork 2
Rapid Endpoints
The ready-made handlers in @tundralibs/rapid/endpoints — liveness,
readiness, metrics, the OpenAPI document and the API reference page — what
each answers, what it takes, and how they behave as ordinary routes.
- Nothing is auto-registered. You mount each handler where you like:
app.get('/healthz', health()). Onlydocs()mounts itself (a page and, withtryItor the Swagger viewer, a script route), so it takes the app. - They are ordinary routes: they appear in OpenAPI, take route middleware
(
app.get(path, authorize(...), metrics())), respect surfaces and versioning, and go through the same error pipeline as your own handlers. -
health()is liveness,ready()is readiness — point the platform's two probes at the two handlers. Both keep the failure cause server-side. -
metrics()answers 503 untilserver.metricsis on;openapi()anddocs()answer 404 outside the modes theirexposenames (DEVELOPMENT only by default). - Session routes (
login,logout,refresh,me) are not here — they come from the pact adapter's factory, so they share one cookie name withauthenticate. See Authentication & authorization.
import { Application } from '@tundralibs/rapid';
import {
docs,
health,
metrics,
openapi,
ready,
} from '@tundralibs/rapid/endpoints';
const app = await Application.initialize({
name: 'api',
server: { metrics: true },
});
app.get('/healthz', health());
app.get('/readyz', ready({ check: () => Promise.resolve() }));
app.get('/metrics', metrics());
app.get('/openapi.json', openapi());
docs(app, { spec: '/openapi.json' });Every handler is a plain RapidHTTPHandler, so the path, the method and any
route middleware are yours. Two consequences worth knowing:
-
Surfaces. On an app with an api surface (
server.api), the JSON handlers exist on both surfaces like any API route; thedocs()page is UI infrastructure (uiOnly), so it exists on the ui surface only. Prefix the probe paths the way your platform expects — nothing is reserved. -
Versioning. A versioned app resolves these routes like any other
(exact → default → unversioned). Mount them unversioned unless you want
/v2/healthzto exist.
GET → 200 { status: 'ok', instance }. instance is app.instanceId,
the per-boot id, handy when several replicas sit behind one address.
| Option | Default | What it does |
|---|---|---|
check |
none |
(ctx) => unknown | Promise<unknown>; a throw or rejection answers 503 { status: 'unhealthy' }. The return value is ignored. |
The cause of a failed check is logged at warn (readiness check failed,
with reason) and never sent: a check usually touches a database or a
downstream whose error text can carry hostnames, DSNs or credentials, and
the probe path is public.
Keep the liveness check cheap or absent — a liveness probe that fails when
the database is slow gets a healthy process restarted. Put dependency checks
on ready().
GET → 200 { status: 'ready', instance }, or 503 in two cases:
| Reply | When |
|---|---|
503 { status: 'draining' } |
app.stop() has begun (app.stopping is true) — checked first, before any check. In-flight requests still complete during the drain. |
503 { status: 'unhealthy' } |
The check threw or rejected; logged like health()'s, never sent. |
| Option | Default | What it does |
|---|---|---|
check |
none | The dependency probe — a pool ping, a cache round-trip. Same signature and disclosure rule as health. |
Use it as the platform's readiness probe: the moment a graceful shutdown
starts, the replica reports not-ready, the load balancer stops routing new
connections to it, and the drain window (shutdownTimeout, seconds — see
the configuration reference) finishes the
requests already in flight.
Serves app.meter — the metro-man registry server.metrics creates — as
Prometheus text (content-type: text/plain; version=0.0.4; charset=utf-8) or JSON.
| Option | Default | Values |
|---|---|---|
format |
'prometheus' |
'prometheus' | 'json' |
With server.metrics off there is no meter, and the endpoint answers a
plain operational 503 { status: 'disabled', message } (not a RAPID_*
error — nothing went wrong). Which series exist depends on the families
enabled: server.metrics: true turns on all seven (requests, errors,
jobs, sockets, middleware, bodies, ui); the object form picks.
Series are prefixed rapid_ (rapid_requests_total,
rapid_request_duration_ms, rapid_job_runs_total, …) — the full list per
family is in the configuration reference.
Your own instruments registered on app.meter.registry are served by the
same endpoint.
The scrape path is usually not for the public: mount it on an internal
port, behind authorize(), or on an api surface your ingress does not
expose.
GET → the assembled OpenAPI 3.0.3 document, built from the registered
routes on first request and cached per version (?version=v2 selects one).
| Option | Default | What it does |
|---|---|---|
info |
title = the app name, version = 1.0.0
|
Merged over the defaults. |
servers |
omitted |
{ url, description? }[]. |
securitySchemes |
none (bearerAuth is added once any route is secured) |
The exact OpenAPI shapes (http / apiKey / oauth2 / openIdConnect), validated at mount. |
expose |
'DEVELOPMENT' |
'PRODUCTION' or 'ALL'; any other mode answers a plain RAPID_NOT_FOUND 404. |
The document is the subject of its own guide — OpenAPI and the API reference — which covers what the routes and decorators contribute, the scheme types and their validation, and versions.
docs(app, options) mounts a page (default /docs) that renders the same
document: server-side from rapid's own templates inside your core/layout,
with an optional credential box and try-it forms, or through a pinned
Scalar / Redoc / Swagger UI shell. Gated by expose like openapi(), takes
guards for an authorized production reference, and never lists itself.
Options, the credential box, the two customization paths and the pitfalls
are in OpenAPI and the API reference.
-
Probes and
exposeare different gates.health()/ready()/metrics()serve in every mode; onlyopenapi()anddocs()are mode-gated. Locking a probe down is your route middleware's job. - A readiness check that writes (a heartbeat row, say) runs on every probe interval — keep both checks read-only and bounded.
-
ready()inside atimeout()scope — a slow dependency check hits the route timeout and answersRAPID_TIMEOUT(a 504), notunhealthy. Either is not-ready to a probe, but the log line differs. -
The reference documents the probes too.
health()and the rest are routes, so they appear in the OpenAPI document; give themopenapi: { tags: ['ops'], security: [] }inapp.route()if you want them grouped and marked public.