Aviary
Client

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.

src/lib/api.ts
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 a toJSON()) → its serialized return shape.
  • Arrays and plain objects recurse; optional properties stay optional.
  • Non-serializable properties (methods, symbol, undefined-only) are dropped, and bigint (which JSON.stringify throws on) maps to never.
  • any / unknown pass 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:

nestjs-codegen.config.ts
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.

On this page