Aviary

Getting Started

Wire the module into your NestJS app and generate a typed client.

Install

pnpm add -D @dudousxd/nestjs-codegen
# a validation adapter (no adapter is bundled in core) — zod shown; or -valibot / -arktype:
pnpm add -D @dudousxd/nestjs-codegen-zod
# the runtime the generated client imports its Fetcher type from:
pnpm add @dudousxd/nestjs-client

If you want TanStack Query helpers in the generated client, install your framework adapter (you almost certainly already have it):

pnpm add @tanstack/react-query   # or @tanstack/vue-query, -svelte-query, -solid-query

Wire the module

This is the recommended setup. Import NestjsCodegenModule into your root module — the codegen starts with your dev server and regenerates the client as you edit your controllers. No config file, no extra process to run.

src/app.module.ts
import { Module } from '@nestjs/common';
import { NestjsCodegenModule } from '@dudousxd/nestjs-codegen/nest';
import { zodAdapter } from '@dudousxd/nestjs-codegen-zod';
import { tanstackQuery } from '@dudousxd/nestjs-codegen-tanstack';

@Module({
  imports: [
    NestjsCodegenModule.forRoot({
      // controllers to scan for routes + contracts
      contracts: { glob: 'src/**/*.controller.ts' },
      // output directory for the generated files
      codegen: { outDir: 'src/generated' },

      validation: zodAdapter,         // zodAdapter | valibotAdapter | arktypeAdapter
      extensions: [tanstackQuery()],  // opt into TanStack Query handles (see below)
    }),
  ],
})
export class AppModule {}

Integrations are extensions you register in extensions: [...]. Besides tanstackQuery() there's nestjsFilterCodegen() (typed filterQuery helpers, see nestjs-filter) and nestjsInertiaCodegen() (Inertia router + navigate(), see nestjs-inertia). Leave the array empty for a plain typed-fetch client. See Extensions.

Now run your app as usual (nest start --watch) and the generated files appear in src/generated — kept in sync while the server runs.

The watcher is a dev/CI concern, so the module skips it automatically when NODE_ENV === 'production'. Force it with enabled: true or turn it off with enabled: false. @nestjs/common is an optional peer dependency — your Nest app already has it.

See Configuration for every option.

Generate in CI

In CI (before you deploy) you want a one-shot, watch-free run that fails the build if the committed client has drifted from your routes. That's the CLI:

npx nestjs-codegen codegen          # one-shot
npx nestjs-codegen codegen --watch  # same watcher the module runs, standalone

The CLI reads nestjs-codegen.config.ts from your project root. To keep a single source of truth, put your options in that file and import it into forRoot():

nestjs-codegen.config.ts
import { defineConfig } from '@dudousxd/nestjs-codegen';
import { zodAdapter } from '@dudousxd/nestjs-codegen-zod';
import { tanstackQuery } from '@dudousxd/nestjs-codegen-tanstack';

export default defineConfig({
  contracts: { glob: 'src/**/*.controller.ts' },
  codegen: { outDir: 'src/generated' },
  validation: zodAdapter,
  extensions: [tanstackQuery()],
});
src/app.module.ts
import codegenConfig from '../nestjs-codegen.config';
// ...
NestjsCodegenModule.forRoot(codegenConfig);

Now the module (dev) and the CLI (CI) share one config. In CI, run the generator and fail on any diff:

npx nestjs-codegen codegen
git diff --exit-code src/generated   # non-zero if the client is stale

pages is Inertia-only and optional — add it only if you build a NestJS + Inertia app (see nestjs-inertia). A plain NestJS API needs just contracts + codegen.

Use the client

The generated api.ts exports a createApi(fetcher) factory — you create the client once, injecting your fetcher:

src/lib/api.ts
import { createApi } from '../generated/api';
import { createFetcher } from '@dudousxd/nestjs-client';

export const api = createApi(createFetcher({ baseUrl: '/api' }));

By default each endpoint is a plain typed fetch — call it and you get a Promise:

users-page.tsx
import { api } from '../lib/api';

const users = await api.users.list();              // typed User[]
const created = await api.users.create({ body });  // typed body + response

Want TanStack Query? Install the extension and register it — each call then returns a handle with .queryOptions() / .mutationOptions():

pnpm add -D @dudousxd/nestjs-codegen-tanstack
src/app.module.ts
import { tanstackQuery } from '@dudousxd/nestjs-codegen-tanstack';

NestjsCodegenModule.forRoot({
  contracts: { glob: 'src/**/*.controller.ts' },
  codegen: { outDir: 'src/generated' },
  extensions: [tanstackQuery()],
});
users-page.tsx
import { useQuery, useMutation } from '@tanstack/react-query';
import { api } from '../lib/api';

const users = useQuery(api.users.list().queryOptions());
const create = useMutation(api.users.create().mutationOptions());

Without the extension the client is TanStack-free (plain fetch). Not using Inertia? Just don't register the inertia extension — there's no @inertiajs/* import unless nestjsInertiaCodegen() is in extensions: [...].

On this page