API Client
The createApi(fetcher) factory and how to use it.
api.ts exports a factory, Tuyau-style: you inject the fetcher at runtime instead
of the codegen hardcoding an import path. This is what makes the client portable — you
control the transport, base URL, auth, and serialization.
import { createApi } from '../generated/api';
import { createFetcher } from '@dudousxd/nestjs-client';
export const api = createApi(
createFetcher({ baseUrl: '/api', headers: () => ({ authorization: token() }) }),
);The result is a nested object keyed by route name (controller.method), fully typed
from your contracts.
Calling endpoints
Each endpoint is an awaitable handle — call it with your typed params/query/body
and await it to perform the request (Tuyau-style). Params, query, and body are all typed
from your contracts:
import { api } from '../lib/api';
// GET /users → Promise<User[]>
const users = await api.users.list();
// GET /users/:id → params are typed + required
const user = await api.users.show({ params: { id } });
// POST /users → body typed from the DTO
const created = await api.users.create({ body: { email: 'a@b.com', password: 'secret123' } });Awaiting the same handle twice is memoized (one network call). It also exposes
.fetch()/.catch()/.finally() if you'd rather not await directly.
Response types & serialization
By default the generated response type reflects the JSON wire shape, not the
in-process server type. A controller that returns a Date doesn't send a Date — it
sends the ISO string Date.prototype.toJSON() produces — so the client awaits a
string. The codegen wraps every response type in the type-only Jsonify<...> utility
from @dudousxd/nestjs-client to model exactly what JSON.parse(JSON.stringify(value))
gives you back:
// Controller: returns { id: string; createdAt: Date }
const user = await api.users.show({ params: { id } });
// user.createdAt is `string`, not `Date` — matching the actual JSON response.Jsonify transforms what JSON genuinely changes and leaves everything else alone:
Date(and anything with atoJSON()) → its serialized return shape.- Arrays and plain objects recurse; optional properties stay optional.
- Non-serializable properties (methods,
symbol,undefined-only) are dropped, andbigint(whichJSON.stringifythrows on) maps tonever. any/unknownpass straight through.
This only affects the response type — body, query, params, and error are
emitted as-is. It's a compile-time type transform with no runtime cost: nothing in
your client code changes, the types just stop lying about Date.
Opt out for superjson clients
If you deserialize responses with superjson,
Date/Map/Set/BigInt are revived on the client, so the raw controller return
type is already correct and Jsonify would be wrong. Set serialization: 'superjson' in
your config to emit the raw types unchanged:
import { defineConfig } from '@dudousxd/nestjs-codegen';
export default defineConfig({
// ...
serialization: 'superjson', // emit raw controller return types (revived on the client)
});Pair this with the runtime superjson hook described in Fetcher & Transports.
TanStack Query helpers
Register the tanstackQuery() extension and the same handle
additionally carries .queryKey(), .queryOptions() (GET) / .mutationOptions() (the rest),
and .infiniteQueryOptions() (GET) — from your TanStack adapter. The call still awaits:
import { useQuery, useMutation, useInfiniteQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
const direct = await api.users.show({ params: { id } }); // still a plain request
const user = useQuery(api.users.show({ params: { id } }).queryOptions());
const list = useInfiniteQuery(api.users.list().infiniteQueryOptions());
const create = useMutation(api.users.create().mutationOptions());
create.mutate({ body: { email: 'a@b.com', password: 'secret123' } });Inferring types
The generated Api type and the per-route response/body types are available without
calling anything:
import type { Api } from '../generated/api';
// the leaf returns an awaitable handle; the (already Jsonify-shaped) response type is:
// Awaited<ReturnType<typeof api.users.show>>Inertia navigation
Register the nestjsInertiaCodegen() extension and the generated
client additionally exposes a typed navigate() helper backed by the Inertia router,
so links and mutations can drive Inertia visits. See nestjs-inertia.