yatta/rpc
Serve a route table, so the server and the client read one definition.
21 exported symbols and 7 members, read from src/types/rpc.ts.
Construct
clientFor
functionBuilds a typed client for a served route table.
The companion to : same table, other side of the wire. Prefer this over calling `createClient` on a table of served routes, because the returned type is the one whose schemas actually resolve.
clientFor<const R extends Record<string, RouteDef>>(routes: R, options?: ClientOptions): ClientFor<R>const api = clientFor(routes, { baseUrl: "/api" });const user = await api.getUser({ params: { id: "42" } });fail
functionThrows a `HttpError` with a readable message, for use inside a handler.
fail(status: number, message: string): neverserve
functionBuilds a router that serves a route table.
serve<const R extends Record<string, RouteDef>>(routes: R, options?: ServeOptions): Routerconst routes = { getUser: serverRoute( { method: "get", path: "/users/:id", params: UserId, response: User }, async ({ params }) => db.users.findById(params.id), ),}; const api = serve(routes, { prefix: "/api" });const client = createClient(routes, { baseUrl });serverRoute
functionDeclares a route with its handler.
serverRoute<const D extends RouteDef>(definition: D, handler: ServerRoute<D>["handler"]): ServerRoute<D>Types
ServeOptions
interfaceinterface ServeOptions6 members
prefixpropertyprefix?: stringPrefix every path is mounted under, e.g. "/api".
validateResponsespropertyvalidateResponses?: booleanValidate a handler's return value against the route's `response` schema.
Off by default because it costs a validation per request. Worth turning on in development: it is the only thing that catches a handler drifting from the shape its clients are typed against.
handlerspropertyhandlers?: Record<string, (input: any, ctx: Context<never>) => HandlerResult | Promise<HandlerResult>>Handlers, keyed by route name.
Kept separate from the route table on purpose. A table with handlers attached cannot be imported by a browser without dragging the server code — `db`, the auth module, whatever — into the client bundle. Split this way, the contract file holds only paths and schemas, both sides import it, and the handlers stay on the server. A route may instead carry its own handler; one attached inline wins over one given here.
middlewarepropertymiddleware?: Array<(ctx: Context<never>, next: () => Promise<Response>) => Promise<Response>>Extra middleware, run before every route.
corspropertycors?: Parameters<Router["cors"]>[0]rateLimitpropertyrateLimit?: { max: number; windowMs: number; key?: (ctx: Context<never>) => string; }Rate limiting applied to every route.
ServerRoute
interfaceA route as the server sees it: the definition plus the handler.
`handler` is typed against the route's own validators, so a handler cannot read a body field the schema does not declare — the error appears where it is made rather than at runtime.
interface ServerRoute<D extends RouteDef>1 member
handlerpropertyhandler: (input: ServerInput<D>, ctx: Context<never>) => HandlerResult | Promise<HandlerResult>
ClientFor
typeThe client type for a served route table.
Takes the same table, so a handler and its client method are two views of one definition rather than two things written twice. Takes the same shape of table as : routes with or without inline handlers. Neither side is widened to `RouteDef` first — doing that made every validator `StandardSchemaV1<any> | undefined`, so `Infer` gave `unknown`, every request body became `unknown` and every response became `any`. The client then accepted any argument and claimed to know nothing, while looking fully typed. A route with an inline handler is unwrapped to its bare definition, and one without is passed through untouched. Both branches are needed: passing a served route through unresolved left every method signature deferred, and unwrapping with a `: RouteDef` fallback widened the schemas away.
type ClientFor<R extends Record<string, RouteDef>> = Client<{ [K in keyof R]: R[K] extends ServerRoute<infer D> ? D : R[K]; }>PathParamsOf
typePath parameters, as plain strings.
type PathParamsOf<Path extends string> = Path extends `${infer _}:${infer Param}/${infer Rest}` ? { [K in Param | keyof PathParamsOf<`/${Rest}`>]: string; } : Path extends `${infer _}:${infer Param}` ? { [K in Param]: string; } : {}ServerInput
typeEverything a handler is given: validated params, query and body.
type ServerInput<D extends RouteDef> = { params: D extends { params: infer P; } ? unknown extends P ? Record<string, string> : Infer<P> : D extends { path: infer Path extends string; } ? PathParamsOf<Path> : Record<string, string>; query: D extends { query: infer Q; } ? unknown extends Q ? Record<string, unknown> : Infer<Q> : Record<string, unknown>; body: D extends { body: infer B; } ? unknown extends B ? unknown : Infer<B> : undefined; ctx: Context<never>; }Other
buildPath
unknownbuildPathClient
unknownContext
unknowncreateClient
unknowncreateClientdefineRoutes
unknowndefineRoutesHandlerResult
unknownHttpMethod
unknownInfer
unknownMethodFor
unknownroute
unknownrouteRouteDef
unknownStandardSchemaV1
unknownAuthConfig or JobPayload — declaring your schema once is enough for the rest to follow. See Typed keys.