Procedures
The two ways to define an API endpoint — short form and full config.
A procedure is a single endpoint in your API. You create them with query(), mutation(), or subscription().
Short form
The simplest way. Pass a schema and a function:
import { } from "zod"
// No input
const = k.query(() => ({ : "ok" }))
// With input validation
const = k.query(
.({ : .() }),
({ , }) => .db.users.findById(.id),
)The first argument is the input schema. Katman validates the input before your function runs. If validation fails, the client gets a 400 error automatically.
Full config
When you need middleware, error definitions, or output validation, use an object:
import { } from "zod"
const = k.mutation({
: [auth], // middleware
: .({ : .() }), // input validation
: .({ : .() }), // output validation (optional)
: { : 409 }, // typed errors
: ({ , , }) => {
if (.db.users.exists(.name)) {
("CONFLICT") // typed — only codes from `errors`
}
return .db.users.create()
},
})What each field does
| Field | Required | Description |
|---|---|---|
resolve | Yes | The function that does the work. Receives { input, ctx, fail, signal }. |
input | No | A schema that validates the incoming data. |
output | No | A schema that validates the return value. |
use | No | An array of guards and wraps to run before the procedure. |
errors | No | A map of error codes to HTTP status codes. Enables the typed fail() function. |
route | No | HTTP route metadata (method, path, description). Used by OpenAPI. |
The resolve function
Every procedure has a resolve function. It receives one object with four properties:
const = k.query({
: ({ , , , }) => {
// input — the validated input (or undefined if no schema)
// ctx — the context object (from context factory + guards)
// fail — throws a typed error (only if `errors` is defined)
// signal — AbortSignal for cancellation
},
})Queries vs mutations
The difference is semantic, not technical. Both work the same way under the hood. The distinction helps with:
- Caching: Queries can be cached. Mutations should not.
- Retries: Queries are safe to retry. Mutations might not be.
- HTTP method: Queries map to GET, mutations to POST (in OpenAPI).
Subscriptions
A subscription is a procedure that returns an async generator. The server sends each yielded value as a Server-Sent Event:
const = k.subscription(async function* () {
for (let = 5; > 0; --) {
yield { : }
await new (() => (, 1000))
}
})The client receives a stream of { count: 5 }, { count: 4 }, ... { count: 1 }.
Routers
Procedures are grouped into routers. You can nest them:
const = k.router({
: healthCheck,
: {
: listUsers,
: createUser,
: deleteUser,
},
: {
: getStats,
},
})The nesting becomes the URL path:
users.list→POST /users/listadmin.stats→POST /admin/stats
Metadata
Attach custom key-value metadata to any procedure using the meta field:
const = k.query({
: z.object({ : z.number().optional() }),
: { : true, : 100 },
: ({ , }) => .db.users.findMany({ : .limit }),
})Metadata is stored on the procedure definition and can be read by middleware:
const = k.wrap(async (, ) => {
// Access meta from the procedure being executed
// Useful for conditional caching, logging, authorization policies
return ()
})Common use cases: cache policies, rate limit tiers, feature flags, authorization rules, and documentation hints.
Export the router type so the client can use it: export type AppRouter = typeof appRouter