Wrap the Fetch Client

Build and ship your own TypeScript client wrapper using the types and the route metadata a generated fetch client carries.

A wrapper builds on the fetch client your app already has, the one kizuna generate writes. Wrap it to build hooks, an SDK layer, or any other integration. @kizunajs/tanstack-query is one of these, and it takes the client and nothing else.

Whether a client goes first-party comes down to demand, not age. If it is something most teams on Kizuna would reach for, we will add it. What we will not take on is one a handful of people need, since we maintain everything we merge. The client API is public, so you can build and publish your own today.

Pattern

src/lib/my-client.ts
import { apiClient } from './api-client';

export function createMyClient() {
    return {
        getUser: (id: string) =>
            apiClient.users.getUser({
                params: {
                    id,
                },
            }),
        createUser: (data: Parameters<typeof apiClient.users.createUser>[0]['body']) =>
            apiClient.users.createUser({
                body: data,
            }),
    };
}

Reading the route off a method

A wrapper that treats routes generically, rather than naming each one, needs to know what each method answers: its method, its path, which statuses it declares. Every client method carries its route under '~route':

const route = apiClient.users.getUser['~route'];

route.method; // 'GET'
route.path; // '/users/:id'
route.streams; // false
Object.keys(route.responses); // ['200', '404']

This is what lets a wrapper take the client alone. A GET gets query semantics, anything else gets mutation semantics, and the declared statuses say which responses are data. responses lists every status the method's Result type does, the automatic 400, 401 and 403 included. A group has no '~route', so it is easy to skip while walking the client.

The property is not enumerable, so it stays out of Object.keys and spreads.

Types

The generated file exports what a wrapper types itself with:

TypeDescription
ClientThe client createClient returns
ClientConfigWhat createClient takes
ClientMethod<Method, Streams, Args, Result>One method, with the HTTP method and whether it streams carried on its '~route'
API.UsersGetUser.Params, Query, Body, HeadersWhat one route's method accepts
API.UsersGetUser.ResultThe discriminated union one route's method resolves to

A wrapper published as a package cannot import a file each app generates, so it reads a method's types off the method itself. ClientMethod is matched by shape, so a package can declare its own copy and infer Method, Streams, Args and Result from any generated client:

type ClientMethod<Method extends string, Streams extends boolean, Args, Result> = ({} extends Args
    ? (args?: Args) => Promise<Result>
    : (args: Args) => Promise<Result>) & {
    readonly '~route': {
        readonly method: Method;
        readonly streams: Streams;
    };
};