Schemas
Routa uses Zod for everything that crosses a boundary: route inputs, middleware inputs, context values middleware provides, and response payloads. A schema is simultaneously the runtime validator, the TypeScript type, and the OpenAPI definition.
import { z } from "zod";
export const CreateUserSchema = z.object({ email: z.email(), name: z.string().min(1),});
export const UserSchema = z.object({ id: z.string(), email: z.email(), name: z.string(),});createRoute({ input: { body: CreateUserSchema, }, responses: { success: { status: 201, schema: UserSchema, }, }, run: async ({ input }) => { return { type: "success", data: await users.create(input.body) }; },});How It Works
Section titled “How It Works”Routa parses each declared input source with schema.parse() before the handler runs, so
input.body is the schema’s output type, not its input type. A schema with a
transform or a default gives the handler the transformed value.
On the way out, Routa always checks the result shape and looks up its type in the
declared responses. By default, it parses data against the matching response schema in
routa dev, so drift produces a generic 500 problem response before malformed data
reaches a client. Production parsing is opt-in with responseValidation: "always".
If production response validation is disabled, Routa does not apply response-side Zod
transforms, defaults, or unknown-key stripping. Middleware provides schemas remain
active in both modes because their parsed values become downstream context. See
Configuration for the full policy.
Where Schemas Live
Section titled “Where Schemas Live”Route-local schemas in a sibling schemas.ts are the default, and scaffolding generates
them that way. When an OpenAPI document declares reusable components.schemas, scaffolding
generates a shared module instead and references it from each route.
Exported schema names must be unique across the project. Routa reports
ROUTA_DUPLICATE_SCHEMA_NAME for collisions, because those names become OpenAPI component
names. Prefer contract-purpose names like CreateUserResponse over generic ones like
Response.
Query Helpers
Section titled “Query Helpers”Query strings arrive as strings, so common list parameters need a parse step. Routa ships two helpers for the two most common cases:
import { Fields, Sort } from "@routa-ts/core/query/helpers";import { z } from "zod";
export const ListUsersQuery = z.object({ sort: Sort(["name", "createdAt"]).optional(), fields: Fields(["id", "name", "email"]).optional(),});Sort accepts a field name with an optional - prefix and produces
{ field, direction }. A request for ?sort=-createdAt gives the handler
{ field: "createdAt", direction: "desc" }, and any field outside the allowed list is
rejected. Fields splits a comma-separated list into a validated array, so
?fields=id,name becomes ["id", "name"].
- Validate at the boundary only. A schema describes what the HTTP contract accepts, not what your domain considers valid; keep business rules in services.
- Keep response schemas narrow. Anything the schema permits is something a client may receive and OpenAPI will advertise.
- Use
responseValidation: "always"when you need production drift detection or depend on response-side Zod transforms and defaults. - Reuse a schema across routes by importing it. Routa deduplicates by exported name, not by structure.
Limits
Section titled “Limits”Zod is the only supported schema library in v0. Constructs Routa cannot represent in
OpenAPI are still enforced at runtime, but generation records a
ROUTA_OPENAPI_UNSUPPORTED_ZOD warning under x-routa-schema-warnings in the generated
document instead of guessing at a definition.