Procedures
Define API endpoints with the builder pattern — $resolve, $input, $use, and more.
A procedure is a single endpoint in your API. You create them with the builder methods $resolve(), $input(), $use(), etc. Subscriptions use subscription().
Simple form
The simplest way. Just pass a resolve function:
import { } from 'zod'
// No input
const = s.$resolve(() => ({ : 'ok' }))
// With input validation
const = s.$input(.({ : .() })).$resolve(({ , }) => .db.users.findById(.id))When you use $input(), Silgi validates the input before your function runs. If validation fails, the client gets a 400 error automatically.
Builder
When you need middleware, error definitions, or output validation, chain builder methods:
import { } from 'zod'
const = s
.$use(auth) // middleware
.$input(.({ : .() })) // input validation
.$output(.({ : .() })) // output validation (optional)
.$errors({ : 409 }) // typed errors
.$resolve(({ , , }) => {
if (.db.users.exists(.name)) {
('CONFLICT') // typed — only codes from `errors`
}
return .db.users.create()
})Available builder methods
| Method | Required | Description |
|---|---|---|
.$resolve(fn) | Yes | The function that does the work. Receives { input, ctx, fail, signal }. |
.$input(schema) | No | A schema that validates the incoming data. |
.$output(schema) | No | Validates the return value and populates the response type in the API reference (Scalar/OpenAPI). Without it, no response schema is generated. |
.$use(...mw) | No | Add 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 | OpenAPI metadata (tags, summary, description). Also used for special-case passthrough routes. |
.$meta({...}) | No | Set custom metadata. |
.$resolve() must be called last — it finalizes the builder and returns a ProcedureDef.
The resolve function
Every procedure has a resolve function. It receives one object with four properties:
const = s.$resolve(({ , , , }) => {
// 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
})OpenAPI metadata
Use $route() to enrich the generated OpenAPI spec with tags, summary, and description. This metadata is purely for documentation — the client always calls procedures via POST on the tree path.
const = s
.$route({ : ['Users'], : 'List users' })
.$input(z.object({ : z.number().optional() }))
.$resolve(({ , }) => .db.users.findMany({ : .limit }))See the OpenAPI page for all available metadata options.
Subscriptions
A subscription is a procedure that returns an async generator. Over HTTP, the server sends each yielded value as a Server-Sent Event. Over WebSocket, each value is a separate message.
// SSE only (HTTP)
const = s.subscription(async function* () {
for (let = 5; > 0; --) {
yield { : }
await new (() => (, 1000))
}
})Subscriptions stream via SSE over HTTP and are automatically exposed over WebSocket — no flag required. The client receives { count: 5 }, { count: 4 }, ... { count: 1 }.
Output schema
$output() does two things: it validates the return value at runtime, and it generates the response schema in the API reference. Without $output, Scalar and the raw OpenAPI spec will have no response type for that procedure.
import { } from 'zod'
const = .({
: .(),
: .(),
: .(),
})
const = s
.$input(.({ : .(), : .() }))
.$output()
.$errors({ : 409 })
.$resolve(({ }) => {
return { : 1, : .title, : .body }
// ^ IDE suggests id, title, body — output schema drives autocomplete
})Routers
Procedures are grouped into routers. You can nest them:
const = s.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 .$meta():
const = s
.$input(z.object({ : z.number().optional() }))
.$meta({ : true, : 100 })
.$resolve(({ , }) => .db.users.findMany({ : .limit }))Metadata is stored on the procedure definition and can be read by middleware:
const = s.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 type AppRouter = typeof appRouter