API reference

yatta/client

A typed client built from your routes. No hand-written fetch calls.

19 exported symbols and 15 members, read from src/types/client.ts.

Construct

buildPath

function

Substitutes `:name` segments and percent-encodes each value.

Encoding per segment rather than the whole path matters: an id containing a slash would otherwise add a path segment, and a value containing `?` or `#` would truncate the URL.

buildPath(template: string, params?: Record<string, unknown>): string

createClient

function

Builds a client for a route table.

createClient<const R extends Routes>(routes: R, options?: ClientOptions): Client<R>
tsts
const routes = defineRoutes({  getUser: route({    method: "get",    path: "/users/:id",    params: z.object({ id: z.string() }),    response: UserSchema,  }),}); const api = createClient(routes, { baseUrl: "https://api.example.com" });const user = await api.getUser({ params: { id: "42" } }); // typed from UserSchema

defineRoutes

function

Declares a table of routes.

Also an identity function, for the same reason.

defineRoutes<const R extends Routes>(routes: R): R

route

function

Declares one route.

An identity function, so the literal keeps its exact types — including which validators were supplied and which were omitted, which is what makes the conditional types above resolve.

route<const D extends RouteDef>(def: D): D

toQueryString

function

Serialises a query object, dropping empty values and expanding arrays.

toQueryString(query: Record<string, unknown>): string

Types

CallError

interface
interface CallError

2 members

  • statusproperty
    status: number
  • bodyproperty
    body: unknown

ClientOptions

interface
interface ClientOptions

6 members

  • baseUrlproperty
    baseUrl?: string

    Origin the routes are called against, e.g. "https://api.example.com".

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

    Called with the raw response before it is read.

    The usual use is a cookie jar, so session auth works the same as it does server-side. Returning headers replaces the default handling.

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

    Read the response headers, for pagination cursors and rate-limit counters.

    Called only on success.

  • expectproperty
    expect?: "json" | "text" | "stream"

    What a response should contain.

    The body is parsed as JSON by default. Set "text" for a plain-text endpoint and "stream" to receive the ReadableStream untouched.

  • headersproperty
    headers?: Record<string, string>

    Sent with every request unless the call overrides it.

  • fetchproperty
    fetch?: typeof globalThis.fetch

    Passed to fetch, so a client can share a cookie jar or agent.

RouteDef

interface

One route: where it goes, what it accepts, and what it returns.

Every validator is optional, because an endpoint legitimately has no body, and an optional field means the route can be declared from as little as a method and a path.

interface RouteDef

7 members

  • methodproperty
    method: HttpMethod
  • pathproperty
    path: string

    Path with `:name` segments, e.g. "/users/:id".

  • paramsproperty
    params?: StandardSchemaV1
  • queryproperty
    query?: StandardSchemaV1
  • bodyproperty
    body?: StandardSchemaV1
  • responseproperty
    response?: StandardSchemaV1<any>

    The response shape.

    Optional, because a route with no declared response should not pretend to return `unknown` — the client falls back to the raw parsed JSON, which is the honest answer.

  • hasBodyproperty
    hasBody?: boolean

    Whether a request body may be sent. Defaults to true for post/put/patch.

Client

type

The client object: one method per route, named by its key in the table.

`R[N]` is passed straight through. Intersecting it with `RouteDef` to force resolution — the usual trick for a deferred indexed access — collapsed each field's type into `Schema & Schema | undefined`, so every query and body came back `unknown`.

type Client<R extends Routes> = { [N in keyof R]: MethodFor<R[N]>; }

HttpMethod

type
type HttpMethod = "get" | "post" | "put" | "patch" | "delete" | "head"

Method

type
type Method<T> = (args: MethodArgs<T>) => Promise<T extends { response: infer R; } ? R : unknown>

MethodArgs

type

Arguments accepted by one method.

Written as an intersection of conditionals rather than a mapped type over `keyof CallArgs<T>`. `keyof` applied to an unresolved conditional type is empty, so every argument vanished from the signature: the client worked at runtime while accepting nothing useful at compile time. Each part contributes its key only when the route declares that input, so passing a `body` to a route without one is an error rather than a field that is quietly dropped on the floor.

type MethodArgs<T> = (T extends { params: infer P; } ? { params: P; } : {}) & (T extends { query: infer Q; } ? { query: Q; } : {}) & (T extends { body: infer B; } ? { body: B; } : {}) & TransportOptions

MethodFor

type

One method, inferred straight from a route definition.

Deliberately not built on : going through a named intermediate type left `R[N]` deferred inside the mapped type, so every signature stayed unresolved and the client rejected correct arguments at compile time while working fine at runtime. Inferring from the definition directly has one fewer layer to defer. Written as two overloads rather than one generic signature with a conditional return. The generic version looked equivalent and was not: TypeScript inferred the argument type from the call and never checked it against the route's schema, so a body of the wrong shape passed a clean compile and failed at runtime. Overloads check each case on its own. The second overload is what `raw: true` selects. With the validator switched off nothing is known about the result, so it is typed `unknown` — the one place the client will not claim to know a shape.

type MethodFor<D extends RouteDef> = [ RequiredArgKeys<D> ] extends [ never ] ? { (args?: Args<D> & { raw?: false; }): Promise<ResponseOf<D>>; (args: Args<D> & { raw: true; }): Promise<unknown>; } : { (args: Args<D> & { raw?: false; }): Promise<ResponseOf<D>>; (args: Args<D> & { raw: true; }): Promise<unknown>; }

PathParams

type

Named parameters a path declares, e.g. { id: string } for "/users/:id".

type PathParams<Path extends string> = Path extends `${infer _Head}:${infer Param}/${infer Rest}` ? { [K in Param | keyof PathParams<`/${Rest}`>]: string; } : Path extends `${infer _Head}:${infer Param}` ? { [K in Param]: string; } : {}

Route

type

A route with its declared parts resolved to concrete types.

type Route<D extends RouteDef> = { method: D["method"]; path: D["path"]; params: D["params"] extends StandardSchemaV1 ? Infer<D["params"]> : PathParams<D["path"]>; query: D["query"] extends StandardSchemaV1 ? Infer<D["query"]> : undefined; body: D["body"] extends StandardSchemaV1 ? Infer<D["body"]> : undefined; response: D["response"] extends StandardSchemaV1 ? Infer<D["response"]> : unknown; hasBody: boolean; }

TransportOptions

type

Options every call accepts.

Separate from the route's own arguments so a route declaring no schemas still has a working signature.

type TransportOptions = { signal?: AbortSignal; headers?: Record<string, string>; raw?: boolean; }

Other

Infer

unknown

isStandardSchema

unknown
isStandardSchema

StandardSchemaV1

unknown
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.