Route Contracts
A route contract is a single object that declares what one HTTP method accepts, every outcome it can produce, which middleware runs before it, and where its handler begins. It is the source of truth from which Routa derives types, runtime validation, generated metadata, and OpenAPI.
import { randomUUID } from "node:crypto";import { createRoute, createRouteRoot } from "@routa-ts/core";import { CreateUserSchema, EmailConflictSchema, UserSchema } from "./schemas.js";import * as users from "../../services/users.js";
const route = createRouteRoot("/users");
export default route({ post: createRoute({ input: { body: CreateUserSchema, }, responses: { success: { status: 201, schema: UserSchema, }, emailConflict: { status: 409, schema: EmailConflictSchema, }, }, run: async ({ input, ctx }) => { const correlationId = randomUUID();
ctx.logger.info("users.create", "Creating a user.", { correlationId, });
const created = await users.createUser(input.body);
if (!created) { ctx.logger.warn("users.create_conflict", "Email is already in use.", { correlationId, }); return { type: "emailConflict", data: { message: "Email already in use." } }; }
ctx.logger.info("users.created", "User created.", { correlationId, userId: created.id, });
return { type: "success", data: created }; }, }),});How It Works
Section titled “How It Works”createRouteRoot(path) binds a file to a URL path and returns a helper for that path. The
helper accepts an object whose keys are HTTP methods, and it rejects any other key. Because
the path is a literal, Routa can look up the context type generated for that path and give
each method the right ctx type.
createRoute(contract) declares one method. It accepts four fields.
An optional map of request sources to Zod schemas. Each source is parsed independently before the handler runs:
| Source | Parsed from |
|---|---|
params |
Dynamic path segments |
query |
The URL query string |
headers |
Request headers, lowercased |
cookies |
The Cookie header, URI-decoded |
body |
The JSON request body |
Only the sources you declare appear on input, and each one arrives as the schema’s parsed
output type. A parse failure returns 400 with a problem document listing the failing
paths, and run never executes.
responses
Section titled “responses”A required map of outcome names to a status and a schema. Names are yours to choose;
success, notFound, and emailConflict are conventions, not keywords. Every method
must declare at least one outcome with a 2xx status.
middleware
Section titled “middleware”An optional ordered array of middleware contracts that run before the handler. Middleware
declared here applies to this method only, in addition to anything inherited from
middleware.ts files. See Middleware.
The handler. It receives { input, ctx } and returns { type, data }, where type is a
key of responses or of any middleware rejects map. Results may contain only those two
fields; Routa rejects extra fields so that responses stay describable in OpenAPI.
Handler Context
Section titled “Handler Context”Every handler receives ctx.logger, Routa’s structural RoutaLogger, regardless of
configuration. When logging is disabled, ctx.logger is a complete no-op implementation
of the same type, so handler code never needs a configuration guard. Middleware adds its
own typed keys to ctx on top of that.
- Declare
get,post,put,patch,delete, andhead. Never declareoptions; Routa generates it from the path’s methods. GETandHEADcontracts cannot declareinput.body.- Response type names must be unique across the method’s own
responsesand therejectsof every middleware in its chain. - Middleware
rejectsmust use 4xx or 5xx statuses. - The route config must be the direct default export of the file. Routa resolves it statically, so indirection through a variable exported later cannot be analyzed.
Limits
Section titled “Limits”Routa owns the HTTP boundary: routing, validation, middleware context, typed responses,
and OpenAPI. Your application owns services, use cases, domain models, persistence, and
business rules. A run body that grows past orchestrating a service call and choosing an
outcome is usually holding logic that belongs outside the route.