Core Reference
@routa-ts/core holds the helpers you write against and the runtime primitives generated
code uses. It exposes four entry points, and hono and zod are peer dependencies rather
than bundled.
npm install @routa-ts/core hono zodpnpm add @routa-ts/core hono zodyarn add @routa-ts/core hono zodbun add @routa-ts/core hono zod| Entry point | Contents |
|---|---|
@routa-ts/core |
createRouta, createRoute, createRouteRoot, createRouteRootFactory, createMiddleware, and the contract types |
@routa-ts/core/hono |
createHonoApp, isBodylessStatus |
@routa-ts/core/logger |
createLogger and the logger types |
@routa-ts/core/query/helpers |
Sort, Fields |
createRouta
Section titled “createRouta”function createRouta<const TConfig extends RoutaConfig>(config: TConfig): TConfig & RoutaConfigPreserves an application configuration object with Routa’s typing. Export the result as
the default export of src/routa.ts, which the runtime imports at boot.
| Option | Type | Default |
|---|---|---|
host |
string |
"127.0.0.1" |
port |
number |
3000 |
logger |
RoutaLogger | false |
Built-in console logger |
lifecycleHeaders |
boolean |
false |
responseValidation |
"development" | "always" |
"development" |
import { createRouta } from "@routa-ts/core";
export default createRouta({ host: "127.0.0.1", port: 3000, lifecycleHeaders: true, responseValidation: "always",});See Configuration for environment precedence.
createRoute
Section titled “createRoute”function createRoute<TInput, TResponses, TMiddleware>( contract: RouteContract<TInput, TResponses, TCtx, TMiddleware>,): RouteContract<TInput, TResponses, TCtx, TMiddleware>Declares one HTTP method. The function returns its argument unchanged; its purpose is to
capture literal types so run receives typed input and ctx and is constrained to the
declared outcomes.
| Field | Type | Required |
|---|---|---|
input |
{ params?, query?, headers?, cookies?, body? } of Zod schemas |
No |
responses |
Record<string, { status: number; schema: ZodType }> |
Yes |
middleware |
readonly MiddlewareContract[] |
No |
deprecation |
{ sunset?: string; replacement?: string } |
No |
run |
({ input, ctx }) => { type, data } |
Yes |
deprecation.replacement accepts a registered route path beginning with / or an
absolute http:/https: URL. Local paths are checked against the generated route
registry.
Errors: returning an undeclared type is a compile error and, at runtime, an
Invalid handler output problem response. Result objects may contain only type and
data.
import { createRoute } from "@routa-ts/core";import { z } from "zod";
export const getStatus = createRoute({ responses: { success: { status: 200, schema: z.object({ ok: z.boolean() }) }, }, run: () => ({ type: "success", data: { ok: true } }),});createRouteRoot
Section titled “createRouteRoot”function createRouteRoot<const TPath extends RegisteredRoutePath>( path: TPath,): <const TConfig extends RouteRootConfigForCtx<Register["routeCtxByPath"][TPath]>>( config: TConfig,) => TConfigBinds a file to a URL path and returns the helper used as the file’s default export. The
returned helper accepts get, post, put, patch, delete, head, and middleware,
and rejects every other key. options is never accepted.
import { createRoute, createRouteRoot } from "@routa-ts/core";import { z } from "zod";import { withAudit } from "../../../middleware/audit.js";
const route = createRouteRoot("/users/:userId");
export default route({ middleware: [withAudit], get: createRoute({ responses: { success: { status: 200, schema: z.object({ id: z.string() }), }, }, run: () => ({ type: "success", data: { id: "usr_1" } }), }),});The path must use :name for dynamic segments even though the filesystem uses $name.
Context types come from the Register augmentation in .routa/routes.gen.ts, so a path
missing from generated metadata produces a type error.
createRouteRootFactory
Section titled “createRouteRootFactory”function createRouteRootFactory< TCtxByPath extends Record<string, Partial<Record<HttpMethod, unknown>>>,>(): <TPath extends keyof TCtxByPath & string>( path: TPath,) => <const TConfig extends RouteRootConfigForCtx<TCtxByPath[TPath]>>( config: TConfig,) => TConfigReturns a createRouteRoot bound to an explicit context map instead of the generated
Register augmentation. Use it in tests, libraries, or tooling that must work without a
generated project. Prefer createRouteRoot in application code.
import { createRoute, createRouteRootFactory } from "@routa-ts/core";import { z } from "zod";
type TestContextByPath = { "/health": { get: Record<never, never> };};
const createTestRouteRoot = createRouteRootFactory<TestContextByPath>();const route = createTestRouteRoot("/health");
export default route({ get: createRoute({ responses: { success: { status: 200, schema: z.object({ ok: z.boolean() }) }, }, run: () => ({ type: "success", data: { ok: true } }), }),});createMiddleware
Section titled “createMiddleware”function createMiddleware<TRequires, TProvides, TRejects, TInput>( contract: MiddlewareContract<TRequires, TProvides, TRejects, TInput>,): MiddlewareContract<TRequires, TProvides, TRejects, TInput>| Field | Type | Purpose |
|---|---|---|
requires |
readonly string[] |
Context keys that must already exist |
provides |
Record<string, ZodType> |
Context keys this middleware adds |
rejects |
Record<string, { status: number; schema: ZodType }> |
Early responses it can return |
input |
Same shape as a route input |
Request data the middleware needs |
openapi |
{ security?, permissions? } |
Metadata emitted into generated OpenAPI |
run |
({ input, ctx, next }) => … |
The implementation |
run either returns a reject outcome or returns next(providedCtx). Routa parses every
declared provides value before merging any of them into ctx, and downstream code
receives the parsed output. Undeclared top-level keys are omitted. A normal z.object()
schema strips unknown nested keys; use .strict() on that schema when unknown nested keys
must reject the request instead.
Errors: next() called more than once throws. A provides value that fails its
schema produces an Invalid handler output response. Reject statuses must be 4xx or 5xx.
import { randomUUID } from "node:crypto";import { createMiddleware } from "@routa-ts/core";import { z } from "zod";
export const withRequestId = createMiddleware({ provides: { requestId: z.string().uuid() }, run: ({ next }) => next({ requestId: randomUUID() }),});createHonoApp
Section titled “createHonoApp”function createHonoApp(routes: readonly HonoRoute[], options?: CreateHonoAppOptions): HonoBuilds the Hono application from route definitions. The CLI runtime calls this for you; call it directly in tests.
| Parameter | Type | Purpose |
|---|---|---|
routes[].method |
HttpMethod |
Lowercase method name |
routes[].path |
string |
Hono path with :name parameters |
routes[].contract |
AnyRouteContract |
The createRoute result |
routes[].createContext |
() => object |
Optional base context, must return a plain object |
options.logger |
RoutaLogger |
Enables http.request and http.error logging |
options.lifecycleHeaders |
boolean |
Emit Deprecation, Sunset, and Link |
options.responseValidation |
"development" | "always" |
Response schema policy; defaults to "development" |
options.runtimeMode |
"dev" | "start" |
Explicit mode; defaults to "dev" for direct use |
Registration throws for an explicitly declared options method, a duplicate
method-and-path pair, a GET or HEAD contract declaring a body, and duplicate reject
keys within one chain.
import { createHonoApp } from "@routa-ts/core/hono";import route from "./routes/users/route.js";
const app = createHonoApp([{ method: "post", path: "/users", contract: route.post }]);isBodylessStatus
Section titled “isBodylessStatus”function isBodylessStatus(status: number): booleanReturns true for 204, 205, and 304. Routa uses it to emit those responses without
a JSON body, since the Response constructor rejects one.
import { isBodylessStatus } from "@routa-ts/core/hono";
export function responseFor(status: number, data: unknown): Response { const body = isBodylessStatus(status) ? null : JSON.stringify(data); return new Response(body, { status });}createLogger
Section titled “createLogger”function createLogger(options?: CreateLoggerOptions): RoutaLogger| Option | Type | Default |
|---|---|---|
enabled |
boolean |
true |
level |
"trace" | "debug" | "info" | "warn" | "error" | "fatal" | "silent" |
"info" |
sink |
(event: RoutaLogEvent) => void |
Console writer |
now |
() => Date |
() => new Date() |
enabled: false forces the level to "silent", which is how the no-op logger is built.
now exists so tests can produce deterministic timestamps.
The returned RoutaLogger is structural and depends on no logging package:
| Member | Signature |
|---|---|
trace, debug, info, warn, error, fatal |
(event: string, message: string, data?: object) => void |
silent |
Same signature, always a no-op |
log |
(event: RoutaLogEvent) => void |
child |
(bindings: object) => RoutaLogger |
bindings |
() => object |
isLevelEnabled |
(level) => boolean |
Every log event carries level, event, message, timestamp, and optional data.
Child bindings are merged into data on each write.
import { createRouta } from "@routa-ts/core";import { createLogger } from "@routa-ts/core/logger";
export default createRouta({ logger: createLogger({ level: "debug", sink: (event) => process.stdout.write(`${JSON.stringify(event)}\n`), }),});Because the contract is structural, any object with these members works — an adapter over Pino, Winston, or a hosted SDK needs no Routa-specific package.
function Sort<const TFields extends readonly [string, ...string[]]>(fields: TFields): ZodTypeBuilds a schema for a sort query parameter. Accepts a field name from fields, with an
optional leading - for descending order, and produces
{ field: TFields[number]; direction: "asc" | "desc" }. Any other field is rejected.
import { Sort } from "@routa-ts/core/query/helpers";import { z } from "zod";
const query = z.object({ sort: Sort(["name", "createdAt"]).optional() });// ?sort=-createdAt -> { field: "createdAt", direction: "desc" }Fields
Section titled “Fields”function Fields<const TFields extends readonly [string, ...string[]]>(fields: TFields): ZodTypeBuilds a schema for a sparse-fieldset query parameter. Splits a comma-separated string,
trims each entry, drops empties, and validates the result against fields.
import { Fields } from "@routa-ts/core/query/helpers";import { z } from "zod";
const query = z.object({ fields: Fields(["id", "name", "email"]).optional() });// ?fields=id, name -> ["id", "name"]RouteRootConfigForCtx
Section titled “RouteRootConfigForCtx”type RouteRootConfigForCtx< TCtxByMethod extends Partial<Record<HttpMethod, unknown>>,> = { middleware?: readonly AnyMiddlewareContract[];} & { [K in HttpMethod]?: ContextualRouteContract< K extends keyof TCtxByMethod ? TCtxByMethod[K] : unknown >;}Describes the route-level middleware and HTTP method contracts accepted by a path-bound route helper. The context map can provide a distinct handler context for each method.
| Field | Type | Purpose |
|---|---|---|
middleware |
readonly AnyMiddlewareContract[] |
Middleware shared by every declared method |
get, post, put, patch, delete, head |
ContextualRouteContract |
Contract typed with that method’s context |
import type { RouteRootConfigForCtx } from "@routa-ts/core";
type UsersRouteConfig = RouteRootConfigForCtx<{ get: { requestId: string }; post: { requestId: string; auth: { userId: string } };}>;Exported Types
Section titled “Exported Types”HttpMethod, RouteInput, RouteResponses, RouteContract, AnyRouteContract,
RouteRun, RouteHandlerArgs, RoutaResult, RouteRunResult, InferInput,
InferResponse, SchemaInput, SchemaOutput, RoutaRouteContext, RoutaConfig,
ResponseValidation, RouteDeprecation, RouteDeprecationReplacement, RegisteredRoutePath,
ExternalApiUrl, ContextualRouteContract, RouteRootConfigForCtx, MiddlewareContract,
AnyMiddlewareContract, MiddlewareRun,
MiddlewareRunArgs, MiddlewareNext, MiddlewareProvidesSpec, MiddlewareProvidesKeys,
MiddlewareProvidedCtx, MiddlewareProvides, MiddlewareRejectsSpec,
MiddlewareRejectResult, MiddlewareOpenApi, InferMiddlewareCtx, and Register.
From @routa-ts/core/hono: HonoRoute, CreateHonoAppOptions. From
@routa-ts/core/logger: RoutaLogger, RoutaLogLevel, RoutaLogLevelWithSilent,
RoutaLogEvent, RoutaLogData, CreateLoggerOptions.