k.routes

Declare routes into one of your groups, with full path-param and response checking.

k.routes.<group>(routes) declares routes into a group from k.groups. Reach a nested group through its parents, k.routes.workspaces.members(...). TypeScript completes every group your Config carries. Call k.routes(routes) for routes in no group.

A route's key is its group's path, then its place inside the call: listUsers below is users.listUsers. Pass what k.routes returns to defineConfig as a list, routes: [usersRoutes, healthRoutes]. A call declared into a group can't be nested inside another call, since its key already comes from its group.

users.ts
import { z } from 'zod';
import { k } from './k';

export const usersRoutes = k.routes.users({
    listUsers: k
        .route({
            method: 'GET',
            path: '/users',
            query: z.object({
                page: z.number().int().min(1).default(1),
                limit: z.number().int().min(1).max(100).default(10),
            }),
            responses: {
                200: z.object({
                    users: z.array(UserSchema),
                    total: z.number(),
                }),
            },
        })
        .handler(/* ... */),
    createUser: k
        .route({
            method: 'POST',
            path: '/users',
            body: z.object({
                name: z.string().min(1),
                email: z.email(),
            }),
            responses: {
                201: UserSchema,
                400: ProblemDetailsSchema,
            },
        })
        .handler(/* ... */),
    getUser: k
        .route({
            method: 'GET',
            path: '/users/:id',
            responses: {
                200: UserSchema,
                404: ProblemDetailsSchema,
            },
        })
        .handler(/* ... */),
});

Route definition fields

FieldRequiredType
methodYes'GET' 'POST' 'PUT' 'PATCH' 'DELETE' 'HEAD' 'OPTIONS'
pathYesURL path. Use :name for path parameters
pathParamsNoZod schema for the path parameters, keyed by placeholder name
bodyNoZod schema for the request body
queryNoZod schema for the query string
headersNoZod schema for request headers
responsesYesResponse schemas keyed by status code. A schema, { body, headers?, contentType? }, or { stream, headers?, contentType? }
contentTypeNo'application/json' (default), 'multipart/form-data', 'application/x-www-form-urlencoded'
summaryNoShort description shown in OpenAPI
descriptionNoLonger description shown in OpenAPI
externalDocsNo{ url, description? }, emitted on the operation
groupsNoMore groups to list the route under, as dotted paths from k.groups
deprecatedNoboolean | string | { message?, date?, link? }, see Deprecations
sunsetNostring | { date, link? }, see Deprecations
rawBodyNotrue to receive the unparsed body as rawBody beside body, see Routes
hiddenNotrue to leave it out of the generated clients and the OpenAPI document, see Routes

Use the { body, headers?, contentType?, cache?, etag? } form of a response to declare typed response headers, a binary body, or a cache policy and entity tag. Use { stream, headers?, contentType? } to send the response piece by piece, see Streaming.

A route's security is resolved from its auth; writing it here is a type error.

Validation

pathParams keys must be exactly the :name placeholders in path. A mismatch is a type error on pathParams, and k.routes throws on it at runtime.

defineConfig throws on a route in a group its groups does not declare, and on two routes that claim the same key. k.routes also throws on:

  • an empty z.object({}) as a body
  • any z.coerce schema, anywhere in the route
  • a structured path parameter (object, array, record, tuple, map, or set)
  • a stream on a 4xx or 5xx status, beside a body, or on a HEAD route
  • a stream whose schema does not fit its contentType: a text/* type takes z.string(), any other non-JSON type takes BinarySchema
  • a stream event name that is empty or contains a line break