API reference

yatta/universal

One route definition, used two ways: called in process or over HTTP.

40 exported symbols and 40 members, read from src/types/universal.ts.

Construct

createApp

function

Builds an app from a table of routes.

createApp<const R extends RouteTable, S extends ServiceMap = ServiceMap>(routes: R, options?: { services?: S; name?: string; middleware?: Middleware<S>[]; }): App<R> & DirectMethods<R> & { readonly serviceType: S; }
tsts
export const app = createApp({  getUser: defineRoute(    { method: "get", path: "/users/:id", params: UserId, response: User },    async ({ params, services }) => services.db.users.findById(params.id),  ),}, {  // Typed, so a handler's `services.db` is your database.  services: { db, auth, realtime },});

createClient

function

Builds the browser client for an app.

This is the other half of the same definition. The app calls handlers in process; this reaches them over HTTP. Both surfaces come from the table, so the developer writes neither.

createClient<const R extends RouteTable>(app: App<R> & DirectMethods<R>, options?: ClientOptions): Client<R>
tsts
// Server Component — direct, no HTTP.const user = await app.getUser({ params: { id } }); // Browser — over HTTP, same shape.const api = createClient(app, { baseUrl: "/api" });const user2 = await api.getUser({ params: { id } });

defaultServices

function

A default service set.

Reads the framework's singletons lazily. A getter rather than a value, so importing a route file does not start a database connection or a socket — the failure mode being that reading a route table on the server to mount it would spin up everything the app owns.

defaultServices(): ServiceMap

defineRoute

function

Declares one route.

The handler returns a plain value, not a `Response`. That is the change that makes a direct call meaningful: `invoke()` hands back the value itself rather than something that has to be unwrapped from a transport. The return type is constrained to the declared response when there is one, so a handler that drifts from its contract fails here rather than at the browser.

defineRoute<const D extends RouteSpec, S extends ServiceMap = ServiceMap, R extends DeclaredOut<D> | Response = DeclaredOut<D>>(spec: D, handler: (input: Input<D, S>) => MaybePromise<R>): Route<D, S, R>
tsts
const getUser = defineRoute(  { method: "get", path: "/users/:id", params: UserId, response: User },  async ({ params, services }) => {    const user = await services.db.users.findById(params.id);    if (!user) throw new HttpError(404, "No such user");    return user;  },);

invoke

async function

Calls a route as a function. No HTTP, no serialization, no connection pool.

This is the point of the whole module. On a server the caller and the handler are in the same process, so a network hop between them buys no isolation and costs a round trip, a socket, and a failure mode.

invoke<N extends string, D extends RouteSpec, S extends ServiceMap, R>(app: App<Record<N, Route<D, S, R>>> & { services: S; }, name: N, args: CallArgs<D>, options?: { ctx?: Context<never>; services?: Partial<S>; }): Promise<Awaited<R>>
tsts
// A Server Component. The handler runs here, with your `db`.const user = await invoke(app, "getUser", { params: { id } }); // Type inferred from the handler, not from a shared interface.const name: string = user.name;

mount

function

Serves an app over HTTP.

The transport. It matches a path, validates the arguments, runs the same handler `invoke` would, and turns the plain value it returns into a response. Nothing about the route is re-declared here.

mount(app: App<any>, options?: TransportOptions): (request: Request) => Promise<Response>
tsts
Bun.serve({ fetch: mount(app) });

routesOf

function

Reads the routes off either an app or a bare table.

Both accepted because both are natural to have on hand, and a developer should not have to know which one a function expects — or unwrap it at the call site.

routesOf(source: App<any> | Record<string, Route>): Record<string, Route>

withMiddleware

function

Adds middleware to a route.

Mutates and returns the route, so it reads as a modifier: `withAuth(defineRoute(...))`.

withMiddleware<S extends ServiceMap, D extends RouteSpec, R>(route: Route<D, S, R>, ...middleware: Middleware<S>[]): Route<D, S, R>

Types

App

interface
interface App<R extends Record<string, any> = RouteTable>

4 members

  • routesproperty
    routes: R
  • servicesproperty
    services: ServiceMap

    The framework objects handlers are given.

  • namesproperty
    names: readonly (keyof R & string)[]

    The names in this app. Useful for generating a client or checking coverage.

  • get
    get<K extends keyof R & string>(name: K): R[K]

    One route by name.

ClientOptions

interface
interface ClientOptions

6 members

  • baseUrlproperty
    baseUrl?: string

    Origin the routes are reached through, e.g. "/api" or a full origin.

  • headersproperty
    headers?: Record<string, string>

    Sent with every request unless a call overrides it.

  • onRequestproperty
    onRequest?: (request: Request) => void

    Runs before each request: a cookie jar, an auth header.

  • onResponseproperty
    onResponse?: (response: Response) => void

    Runs after a successful response: pagination cursors, rate-limit counters.

  • fetchproperty
    fetch?: typeof globalThis.fetch

    Passed to fetch, to share a cookie jar or an agent.

  • validateResponsesproperty
    validateResponses?: boolean

    Validate a response against its declared schema.

    On by default. It is the only thing that turns a server that changed into an error naming the field, instead of an `undefined` three components later.

Route

interface

A declared route: its spec, its handler, and the handler's return type kept so

interface Route<D extends RouteSpec = RouteSpec, S extends ServiceMap = ServiceMap, R = unknown>

3 members

  • specproperty
    spec: D
  • handlerproperty
    handler: (input: Input<D, S>) => MaybePromise<R>

    Replaced once, by `createApp`, with the middleware chain around it.

  • middlewareproperty
    middleware?: Middleware<S>[]

    Checks that apply to this route only.

    Run inside the app's own middleware, never instead of it, so a route-level check cannot accidentally shadow a cross-cutting one.

ServiceMap

interface

The framework objects a handler can use.

Injected rather than imported, so a handler can be called directly, over HTTP, or in a test with a stand-in. An open map by default, because which engines an app uses is its own business.

interface ServiceMap

8 members

  • dbproperty
    db?: unknown
  • authproperty
    auth?: unknown
  • realtimeproperty
    realtime?: unknown
  • cacheproperty
    cache?: unknown
  • jobsproperty
    jobs?: unknown
  • mailproperty
    mail?: unknown
  • storageproperty
    storage?: unknown
  • observerproperty
    observer?: unknown

Services

interface

Services with the engines an app actually has, so handlers get real types.

interface Services

8 members

  • dbproperty
    db: unknown
  • authproperty
    auth: unknown
  • realtimeproperty
    realtime: unknown
  • cacheproperty
    cache: unknown
  • jobsproperty
    jobs: unknown
  • mailproperty
    mail: unknown
  • storageproperty
    storage: unknown
  • observerproperty
    observer: unknown

TransportOptions

interface

4 members

  • basePathproperty
    basePath?: string

    Stripped before matching, when the mount point is not in the URL.

  • validateResponsesproperty
    validateResponses?: boolean

    Validate each response against its declared schema. Costs one check.

  • onNotFoundproperty
    onNotFound?: (req: WireRequest) => WireResponse | Promise<WireResponse>

    Called when no route matches. Defaults to a 404 JSON body.

  • onErrorproperty
    onError?: (error: unknown, req: WireRequest, response: WireResponse) => void

    Called with a thrown error and the response it became.

WireRequest

interface

A request, as far as the transport layer needs to know.

interface WireRequest

4 members

  • methodproperty
    method: string
  • urlproperty
    url: string
  • headersproperty
    headers: Record<string, string>
  • bodyproperty
    body?: string

WireResponse

interface
interface WireResponse

3 members

  • statusproperty
    status: number
  • headersproperty
    headers: Record<string, string>
  • bodyproperty
    body: string | Uint8Array | ReadableStream | null

CallArgs

type

Arguments for a direct call.

What a route accepts, minus the request context: there is no HTTP request, because nothing crossed a network. Each input is contributed only when the route declares it, so passing a body to a route without one is an error rather than a field quietly dropped.

type CallArgs<D extends RouteSpec> = ParamsArg<D> & QueryArg<D> & BodyArg<D> & { ctx?: Context<never>; signal?: AbortSignal; }

CallResult

type

What a direct call hands back: the handler's own return type.

type CallResult<T> = T extends Route<any, any, infer R> ? Awaited<R> : never

Client

type

A client for an app.

type Client<R extends RouteTable> = HttpMethods<R>

DeclaredOut

type

The value a handler returns when a response schema is declared.

type DeclaredOut<D extends RouteSpec> = D extends { response: infer Res; } ? unknown extends Res ? unknown : Infer<Res> : unknown

DirectMethods

type

A set of routes plus the services they use.

One of these per process on the server, and per request on a server render — never a module-level singleton that outlives a request, which would serve one user's data to the next. One method per route, each callable directly. Derived from the table, so the developer writes routes and nothing else — no wrapper per endpoint, no name string to keep in step, no signature to maintain. The method's argument and return types come from the route itself.

type DirectMethods<R extends RouteTable> = { [K in keyof R & string]: (args?: CallArgs<R[K]["spec"]>) => Promise<CallResult<R[K]>>; }

HttpMethods

type

A method per route, reached over HTTP.

The same names, the same arguments and the same return types as the app's direct methods. Derived from the same table, so there is nothing to keep in step — a renamed route is a compile error here too.

type HttpMethods<R extends RouteTable> = { [K in keyof R & string]: (args?: CallArgs<R[K]["spec"]>) => Promise<DeclaredOut<R[K]["spec"]>>; }

Input

type

Everything a handler is given.

type Input<D extends RouteSpec, S extends ServiceMap> = { params: D extends { params: infer P; } ? unknown extends P ? D extends { path: infer Path extends string; } ? PathParams<Path> : Record<string, string> : Infer<P> : D extends { path: infer Path extends string; } ? PathParams<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>; services: S; }

Middleware

type

Runs before a handler, on both paths.

Return a value to answer instead of calling the handler — that is how an auth check rejects. Return `undefined` to continue. The point of it being here rather than only on the transport: a check that lives in HTTP middleware is *not* run by `app.getUser()` or `invoke()`. A Server Component calling a route directly would sail straight past an authorisation check that HTTP enforces, which is the worst possible place for that gap — the code looks guarded and is not.

type Middleware<S extends ServiceMap = ServiceMap> = (input: { name: string; ctx: Context<never>; services: S; args: Record<string, unknown>; }) => unknown | Promise<unknown>

PathParams

type

Path parameters as plain strings, read from the path template.

type PathParams<Path extends string> = Path extends `${infer _H}:${infer P}/${infer R}` ? { [K in StripOptional<P> | keyof PathParams<`/${R}`>]: string; } & Partial<Record<StripOptional<P>, string>> : Path extends `${infer _H}:${infer P}` ? PathParamsOne<P> : {}

RouteSpec

type

The shape a route declares: method, path, and validators.

RouteTable

type

A table of routes.

type RouteTable = Record<string, Route<RouteSpec, any, any>>

Other

buildPath

unknown
buildPath

buildPathFrom

unknown
buildPathFrom

bySpecificity

unknown
bySpecificity

extractParams

unknown
extractParams

HttpMethod

unknown

matchesPath

unknown
matchesPath

ParsedPath

unknown

parseTemplate

unknown
parseTemplate

PathTemplateError

unknown

readQuery

unknown
readQuery

RouteDef

unknown

Segment

unknown

toQueryString

unknown
toQueryString
Tip
Most of the types above are inferred. You rarely import AuthConfig or JobPayload — declaring your schema once is enough for the rest to follow. See Typed keys.