Silgi

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

MethodRequiredDescription
.$resolve(fn)YesThe function that does the work. Receives { input, ctx, fail, signal }.
.$input(schema)NoA schema that validates the incoming data.
.$output(schema)NoValidates the return value and populates the response type in the API reference (Scalar/OpenAPI). Without it, no response schema is generated.
.$use(...mw)NoAdd guards and wraps to run before the procedure.
.$errors({...})NoA map of error codes to HTTP status codes. Enables the typed fail() function.
.$route({...})NoOpenAPI metadata (tags, summary, description). Also used for special-case passthrough routes.
.$meta({...})NoSet 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.listPOST /users/list
  • admin.statsPOST /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 the router type so the client can use it: export type AppRouter = typeof appRouter

On this page