Askr documentation
Server & APIs

Typed Clients

Typed Clients at the Askr server boundary, with validated input and explicit failure states.

Example

Describe endpoint input and result once, then create a client whose calls accept typed params, query, headers, body, and AbortSignal.

import { createClient, defineApi, get, json } from '@askrjs/fetch';

const definition = defineApi({
  getProject: get('/projects/{id}')
    .params<{ id: string }>()
    .returns(json<Project>()),
});
const api = createClient(definition, { baseUrl: env.API_URL });

const result = await api.getProject({ params: { id }, signal });
if (result.ok) renderProject(result.data);

Define endpoints

get(path), post(path), and the other method builders return an EndpointBuilder that you refine by chaining .params<T>(spec?), .query<T>(spec?), .headers<T>(spec?), .body(codec), .returns(codec) or .returns(status, codec), and .errors({status: codec}). defineApi(endpoints, metadata?) accepts a plain object of these builders — or already-built EndpointDescriptor values — and returns the ApiDefinition that createClient() consumes.

Create a client

createClient(api, options) builds a client from ClientOptions: baseUrl, a custom fetch implementation, default headers, credentials, a default timeout, and a middleware array. The returned object exposes one method per endpoint key, so client.getUser(...) exists only if you named an endpoint getUser in defineApi().

Typed input

Each client method takes a single input object assembled from whichever of params, query, headers, and body the endpoint declared. params and body are required whenever the endpoint has them; query and headers become required as soon as at least one of their fields is required, not only when every field is — an object with one required field and several optional ones still makes the whole query or headers object mandatory. When an endpoint needs none of these, its method's input argument itself becomes optional, so client.health() can be called with no arguments at all.

Typed output

A call resolves to a discriminated ClientResult: a SuccessResult keyed by whichever status you registered with .returns(), an HttpResult keyed by whichever status or default you registered with .errors(), or a FailureResult for anything below the HTTP layer — a request-building error (a missing path parameter, for instance), a network error, a timeout, a caller abort, a decode failure, or a middleware error. Narrowing on result.ok or result.kind gives you the exact typed data or error shape for that branch without a cast.