@mrzr/api-client

TypeScript

Type responses, paginated lists, ordering params, the auth user and plugin methods with @mrzr/api-client, and build a typed API layer on top of it.

The package is written in TypeScript and ships its own types. Every type is exported from @mrzr/api-client.

Typing responses

The generic on each method types res.data:

const { data } = await api.get<User[]>("/users");
//      ^? User[] | undefined

data is optional because a 204, an empty body or a failed parse legitimately has none. With the default throwError: true, a failure has already thrown by the time you read it, so data! is safe after a successful await.

The generic also types the transforms: afterFunc and beforeSelectOptions receive the payload as R.

Lists and ordering

ListResponse<T> is the common paginated shape (Django REST Framework's), and ordering in params is typed against the item, not the wrapper:

import type { ListResponse } from "@mrzr/api-client";

const { data } = await api.get<ListResponse<User>>("/users", {
  params: { page: 2, ordering: { createdAt: "desc" } },   // keys come from User
});
data?.results;   // User[]
data?.count;     // number
data?.next;      // string | null
interface ListResponse<T> { count: number; next: string | null; previous: string | null; results: T[] }
type Ordering<T> = { [K in keyof T]?: "asc" | "desc" };

Other params stay untyped ([key: string]: unknown), so any query parameter is allowed.

A typed API layer

Wrap each resource once, and components never see URLs:

api/users.ts
import { api } from "@/lib/api";
import type { ListResponse } from "@mrzr/api-client";

export interface User { id: number; name: string; email: string }
export type NewUser = Omit<User, "id">;

export const users = {
  list: (page = 1) => api.get<ListResponse<User>>("/users", { params: { page } }).then((r) => r.data!),
  byId: (id: number) => api.get<User>("/users/{id}", { addTemplateToUrl: { id } }).then((r) => r.data!),
  create: (input: NewUser) => api.post<User>("/users", input).then((r) => r.data!),
  update: (id: number, patch: Partial<NewUser>) =>
    api.patch<User>("/users/{id}", patch, { addTemplateToUrl: { id } }).then((r) => r.data!),
  remove: (id: number) => api.delete("/users/{id}", { addTemplateToUrl: { id } }),
};

Typing the user

AuthState.user is unknown, because it's whatever your server returns. Narrow it in one place:

import type { AuthState } from "@mrzr/api-client";

interface AppUser { id: number; email: string; role: "admin" | "member" }
export type AppAuthState = Omit<AuthState, "user"> & { user?: AppUser };

api.onAuthStateChange((s) => setAuth(s as AppAuthState));

Narrowing errors

import { ApiError } from "@mrzr/api-client";

export const isStatus = (e: unknown, code: number): e is ApiError =>
  e instanceof ApiError && e.statusCode === code;

if (isStatus(err, 409)) showConflictDialog(err.message);

ApiError is a real subclass of Error with name === "ApiError", so the name check also works across bundles. Its errors are typed Record<string, string[]>, and data is unknown.

Plugin methods

Methods a plugin adds are typed on the client automatically. For the services plugin, a misspelled service name is a compile error:

const api = createClient({ plugins: [services({ files: "https://files.example.com" })] });
api.service("files");   // ok
api.service("fiels");   // error

To type your own, see adding typed methods. PluginExtensions<typeof plugins> is the type of the methods a plugin list adds.

Exported types

TypeIs
ApiClientThe client createClient returns
ClientOptions, RequestConfig<T>Client and per-request options
IRes<R>The response envelope
ApiErrorThe error class (a value, not just a type)
HttpMethod, ResponseFormat, AuthMode, StorageKindString unions
Params<T>, Ordering<T>, ListResponse<T>Query and list helpers
AuthState, TokenPair, TokenStorageAuth state, tokens and storage adapters
TokenExtractor, TokenFieldMap, RefreshBodyConfigCustom token shapes
CancelOptions, CancelSelector, CancelMatch, CancelScope, PendingRequestCancellation
SocketTokenOptionsOptions for getSocketToken
LogEntryWhat onLog receives
ApiPlugin<E>, PluginRequest, PluginExtensions<P>Plugins
ServiceOptions, ServiceDefinition, ServiceClientFrom @mrzr/api-client/services

On this page