Katman

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

FieldRequiredDescription
resolveYesThe function that does the work. Receives { input, ctx, fail, signal }.
inputNoA schema that validates the incoming data.
outputNoA schema that validates the return value.
useNoAn array of guards and wraps to run before the procedure.
errorsNoA map of error codes to HTTP status codes. Enables the typed fail() function.
routeNoHTTP 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.listPOST /users/list
  • admin.statsPOST /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

On this page