Skip to content

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.

Terminal window
npm install @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
function createRouta<const TConfig extends RoutaConfig>(config: TConfig): TConfig & RoutaConfig

Preserves 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"
src/routa.ts
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.

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 } }),
});
function createRouteRoot<const TPath extends RegisteredRoutePath>(
path: TPath,
): <const TConfig extends RouteRootConfigForCtx<Register["routeCtxByPath"][TPath]>>(
config: TConfig,
) => TConfig

Binds 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.

src/routes/users/$userId/route.ts
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.

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,
) => TConfig

Returns 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 } }),
}),
});
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() }),
});
function createHonoApp(routes: readonly HonoRoute[], options?: CreateHonoAppOptions): Hono

Builds 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 }]);
function isBodylessStatus(status: number): boolean

Returns 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 });
}
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.

src/routa.ts
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): ZodType

Builds 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" }
function Fields<const TFields extends readonly [string, ...string[]]>(fields: TFields): ZodType

Builds 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"]
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 } };
}>;

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.