http/route_spec.ts

Introspectable route spec system for Hono apps.

Routes are defined as data (method, path, auth, input/output schemas, handler), then applied to Hono. The attack surface is generated from the specs — always accurate, always complete.

Input/output schemas align with SAES ActionSpec conventions:

  • input: Zod schema for the request body (z.null() for no body)
  • output: Zod schema for the success response body
  • z.strictObject() for inputs (reject unknown keys)
view source

Declarations
#

13 declarations

apply_middleware_specs
#

http/route_spec.ts view source

(app: Hono<BlankEnv, BlankSchema, "/">, specs: MiddlewareSpec[]): void import {apply_middleware_specs} from '@fuzdev/fuz_app/http/route_spec.js';

Apply named middleware specs to a Hono app.

app

type Hono<BlankEnv, BlankSchema, "/">

specs

returns

void

mutates

  • app —

apply_route_specs
#

http/route_spec.ts view source

(app: Hono<BlankEnv, BlankSchema, "/">, specs: RouteSpec[], resolve_auth_guards: AuthGuardResolver, log: Logger, db: Db, authorize?: AuthorizationHandler | undefined): void import {apply_route_specs} from '@fuzdev/fuz_app/http/route_spec.js';

Apply route specs to a Hono app.

For each spec: resolves auth to guards via the provided resolver, adds input validation middleware (for routes with non-null input schemas), runs the optional authorization phase to resolve the acting actor + build the request context, wraps handler with DEV-only output and error validation, wraps with error catch layer (catches ThrownJsonrpcError and generic errors), and registers the route.

Per-route middleware order: content type (415, POST only) → params → query → pre-authorization auth guards (401 + rule-3 scope) → authorization phase → post-authorization auth guards (403) → input validation (400) → handler. The content type goes first because it says almost nothing about the route, and a forged request should be refused before any credential is read. Body validation runs behind every authority gate, so a caller any of them refuse never sees route-shape information from a parse failure — a 400 there would confirm the route exists and describe how to call it. Params and query still validate first (they address the route rather than carry its payload, and the authorization phase reads acting off the query on GETs).

Each handler receives a RouteContext with:

  • db: transaction-scoped when RouteSpec.transaction is true; pool-level otherwise
  • pending_effects: eager fire-and-forget pool-write queue
  • post_commit_effects: deferred-thunk queue (push via emit_after_commit)

Also enforces registry-time invariant 2 from the auth-shape design: auth.actor !== 'none' ⟺ input or query declares acting?: ActingActor. REST is bi-located (GETs declare acting on query, mutations on input), so the check passes both slots; the action-dispatcher registries (compile_action_registry) share the same helper with input only — ActionSpec has no query shape.

app

type Hono<BlankEnv, BlankSchema, "/">

specs

type RouteSpec[]

resolve_auth_guards

log

type Logger

db

used for transaction wrapping and RouteContext

type Db

authorize?

optional authorization phase; runs after the pre-authorization guards and before the post-authorization guards

optional

returns

void

throws

  • Error - if two specs share the same `method` + `path` (each combination must be unique), or if any spec violates the actor-acting biconditional

mutates

  • app —

AuthGuardResolver
#

http/route_spec.ts view source

AuthGuardResolver import type {AuthGuardResolver} from '@fuzdev/fuz_app/http/route_spec.js';

Resolves a RouteAuth to middleware guard handlers.

Injected into apply_route_specs to decouple route registration from auth-specific middleware. See fuz_auth_guard_resolver in auth/auth_guard_resolver.ts for the standard implementation.

(call)

type (auth: { account: "optional" | "none" | "required"; actor: "optional" | "none" | "required"; roles?: readonly string[] | undefined; credential_types?: readonly string[] | undefined; required_scope?: string | undefined; }): AuthGuards

auth

returns AuthGuards

AuthGuards
#

http/route_spec.ts view source

AuthGuards import type {AuthGuards} from '@fuzdev/fuz_app/http/route_spec.js';

Two-phase auth guard set returned by AuthGuardResolver.

pre_authorization runs before the authorization phase — the 401 check and the rule-3 token-scope refusal live here, so a caller either one turns away never reaches the actor resolution or the route's body. post_authorization runs after the authorization phase has populated RequestContext — credential / role checks live here because they read c.var.request_context.role_grants.

Both phases run ahead of body validation, which is why neither is named for it: route-shape information stays behind every authority gate. The names say which side of the authorization phase a guard sits on, the only axis that still distinguishes them.

pre_authorization

type MiddlewareHandler[]

post_authorization

type MiddlewareHandler[]

AuthorizationHandler
#

http/route_spec.ts view source

AuthorizationHandler import type {AuthorizationHandler} from '@fuzdev/fuz_app/http/route_spec.js';

Per-route authorization phase. Runs after the pre-authorization auth guards and before input validation; resolves the acting actor (when `auth.actor !== 'none') from the acting selector — c.var.validated_query.acting` on GETs, read off the raw body on mutations — and sets the request context on the Hono context. Per-route order in apply_route_specs: content type (415) → params → query → pre-authorization auth (401 + rule-3 scope) → authorization phase → post-authorization auth (403) → input validation (400) → handler.

Returns a Response to short-circuit (resolution failure → 400 / 500), or void to continue. The http framework stays auth-agnostic — fuz_app provides the implementation via create_fuz_authorization_handler in auth/request_context.ts.

(call)

type (c: Context<any, any, {}>, spec: RouteSpec): Promise<void | Response>

c

type Context<any, any, {}>

spec

returns Promise<void | Response>

get_route_input
#

http/route_spec.ts view source

<S extends z.ZodType>(c: Context<any, any, {}>, schema: S): output<S> import {get_route_input} from '@fuzdev/fuz_app/http/route_spec.js';

Get validated input from the Hono context.

Call after the input validation middleware has run. Pass the route's input Zod schema to infer the typed shape directly:

const input = get_route_input(c, my_route_spec.input);

Or pass an explicit type argument when the schema isn't in scope:

const input = get_route_input<MyInput>(c);

c

type Context<any, any, {}>

schema

type S

overloads

<S extends z.ZodType>(c: Context<any, any, {}>, schema: S): output<S>

Get validated input from the Hono context.

Call after the input validation middleware has run. Pass the route's input Zod schema to infer the typed shape directly:

const input = get_route_input(c, my_route_spec.input);

Or pass an explicit type argument when the schema isn't in scope:

const input = get_route_input<MyInput>(c);
c Context<any, any, {}>
schema S
returns output<S>
<T = unknown>(c: Context<any, any, {}>): T
c Context<any, any, {}>
returns T

returns

output<S>

generics

get_route_input<S extends z.ZodType>
S
constraint z.ZodType

get_route_params
#

http/route_spec.ts view source

<S extends z.ZodType>(c: Context<any, any, {}>, schema: S): output<S> import {get_route_params} from '@fuzdev/fuz_app/http/route_spec.js';

Get validated URL path params from the Hono context.

Call after the params validation middleware has run. Pass the route's params schema to infer the typed shape, or supply an explicit type argument. See get_route_input for the two call shapes.

c

type Context<any, any, {}>

schema

type S

overloads

<S extends z.ZodType>(c: Context<any, any, {}>, schema: S): output<S>

Get validated URL path params from the Hono context.

Call after the params validation middleware has run. Pass the route's params schema to infer the typed shape, or supply an explicit type argument. See get_route_input for the two call shapes.

c Context<any, any, {}>
schema S
returns output<S>
<T = unknown>(c: Context<any, any, {}>): T
c Context<any, any, {}>
returns T

returns

output<S>

generics

get_route_params<S extends z.ZodType>
S
constraint z.ZodType

get_route_query
#

http/route_spec.ts view source

<S extends z.ZodType>(c: Context<any, any, {}>, schema: S): output<S> import {get_route_query} from '@fuzdev/fuz_app/http/route_spec.js';

Get validated URL query params from the Hono context.

Call after the query validation middleware has run. Pass the route's query schema to infer the typed shape, or supply an explicit type argument. See get_route_input for the two call shapes.

c

type Context<any, any, {}>

schema

type S

overloads

<S extends z.ZodType>(c: Context<any, any, {}>, schema: S): output<S>

Get validated URL query params from the Hono context.

Call after the query validation middleware has run. Pass the route's query schema to infer the typed shape, or supply an explicit type argument. See get_route_input for the two call shapes.

c Context<any, any, {}>
schema S
returns output<S>
<T = unknown>(c: Context<any, any, {}>): T
c Context<any, any, {}>
returns T

returns

output<S>

generics

get_route_query<S extends z.ZodType>
S
constraint z.ZodType

prefix_route_specs
#

http/route_spec.ts view source

(prefix: string, specs: RouteSpec[]): RouteSpec[] import {prefix_route_specs} from '@fuzdev/fuz_app/http/route_spec.js';

Prepend a prefix to all route spec paths.

prefix

the path prefix (e.g. /api/account)

type string

specs

type RouteSpec[]

returns

RouteSpec[]

a new array — the input specs are not mutated

RouteContext
#

http/route_spec.ts view source

RouteContext import type {RouteContext} from '@fuzdev/fuz_app/http/route_spec.js';

Per-request deps provided by the framework to route handlers.

Audit writes and other rollback-resilient fire-and-forget calls run through AppDeps.audit.emit(ctx, input) (see auth/audit_emitter.ts), which captures the pool inside its closure — handlers can never accidentally write audits against the request transaction.

Routes whose body manages its own transaction (signup, bootstrap) declare transaction: false on the spec, which makes route.db the pool — they reach for it directly.

db

Transaction-scoped when RouteSpec.transaction is true (the default for non-GET); pool-level otherwise.

type Db

pending_effects

Eager fire-and-forget queue — push the in-flight Promise<void> for pool writes already running (audit emits, api-token usage tracking). The flush middleware drains via flush_pending_effects after the handler returns.

type Promise<void>[]

post_commit_effects

Deferred post-commit thunks — do not push directly; reach for emit_after_commit(ctx, fn) from http/pending_effects.ts. The flush middleware invokes each thunk after the handler (and any wrapping db.transaction) returns, closing the microtask-ordering window that an eager Promise.resolve().then(fn) leaves open inside the transaction.

type (() => void | Promise<void>)[]

RouteHandler
#

http/route_spec.ts view source

RouteHandler import type {RouteHandler} from '@fuzdev/fuz_app/http/route_spec.js';

Route handler function — receives the Hono context and a RouteContext with per-request deps (db, pending_effects).

TypeScript allows fewer params, so handlers that don't need route can use (c) => ... without changes.

(call)

type (c: Context<any, any, {}>, route: RouteContext): Response | Promise<Response>

c

type Context<any, any, {}>

route

returns Response | Promise<Response>

RouteMethod
#

http/route_spec.ts view source

RouteMethod

type "GET" | "POST" | "PUT" | "DELETE" | "PATCH"

import type {RouteMethod} from '@fuzdev/fuz_app/http/route_spec.js';

HTTP methods supported by route specs.

RouteSpec
#

http/route_spec.ts view source

RouteSpec import type {RouteSpec} from '@fuzdev/fuz_app/http/route_spec.js';

A single route definition — the unit of the surface map.

input and output schemas align with SAES ActionSpec naming. Use z.null() for routes with no request body (GET, DELETE without body).

method

type RouteMethod

path

type string

auth

type RouteAuth

handler

type (c: Context<any, any, {}>, route: RouteContext): Response | Promise<Response>

c

type Context<any, any, {}>

route

returns Response | Promise<Response>

description

type string

params?

URL path parameter schema. Use z.strictObject() with string fields matching :param segments.

REST-only — actions dispatch through a single JSON-RPC endpoint and encode everything in input, so params doesn't appear on ActionSpec.

type ZodObject<$ZodLooseShape, $strip>

query?

URL query parameter schema. Use z.strictObject() with string fields.

type ZodObject<$ZodLooseShape, $strip>

input

Request body schema. Use z.null() for routes with no body.

type ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>

output

Success response body schema.

type ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>

raw_body?

Marks a route whose request and/or response carries raw bytes or a streaming protocol rather than JSON — git smart-HTTP, file-store binary uploads/downloads, raw internal callbacks. Disambiguates the overloaded input: z.null(), which otherwise can't distinguish "no body" (GET /health) from "raw bytes" (a binary upload).

A raw_body POST takes any Content-Type a browser can't send cross-origin without a preflight; every other POST route requires application/json, bodyless or not (415 unsupported_content_type otherwise — see http/simple_request.ts). The schema-driven round-trip test suite auto-skips raw_body routes (it can neither synthesize a meaningful body nor assert a JSON output shape), so consumers don't hand-maintain a skip_routes entry per binary route. Also surfaces in AppSurfaceRoute so generated docs render "raw" instead of a misleading null body.

type boolean

rate_limit?

Rate limit key type — declares what this route's rate limiter is keyed on.

When set, 429 (RateLimitError) is auto-derived in derive_error_schemas. The actual RateLimiter instance is still wired imperatively in the handler — this field is metadata for surface introspection and policy invariants.

type RateLimitKey

errors?

Handler-specific error response schemas keyed by HTTP status code.

Middleware errors (auth 401/403, validation 400, rate limit 429) are auto-derived from auth, input, and rate_limit. Declare handler-specific errors here (e.g., 404 for not-found, 409 for conflicts).

Explicit entries override auto-derived ones for the same status code.

type Partial<Record<number, ZodType<unknown, unknown, $ZodTypeInternals<unknown, unknown>>>>

transaction?

Whether to wrap the handler in a database transaction.

When omitted, defaults are derived from the HTTP method:

  • GET → false (read-only, no transaction)
  • All others (POST, PUT, DELETE, PATCH) → true

Set explicitly to override the default (e.g., false for a POST that manages its own transaction like signup).

type boolean

Depends on
#

Imported by
#