Skip to content

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.

src/routes/users/schemas.ts
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(),
});
src/routes/users/route.ts
createRoute({
input: {
body: CreateUserSchema,
},
responses: {
success: {
status: 201,
schema: UserSchema,
},
},
run: async ({ input }) => {
return { type: "success", data: await users.create(input.body) };
},
});

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.

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 strings arrive as strings, so common list parameters need a parse step. Routa ships two helpers for the two most common cases:

src/routes/users/schemas.ts
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.

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.