Aviary

Configuration

Every option in nestjs-codegen.config.ts.

These are the same options you pass to NestjsCodegenModule.forRoot(). The module is the primary way to run the codegen in dev; this config file is what the CLI loads for CI runs. Author it with defineConfig (the legacy nestjs-inertia.config.ts name is still accepted), and import it into forRoot() to keep a single source of truth.

forRoot() accepts two module-only fields on top of everything below: enabled (boolean — defaults on outside NODE_ENV=production) and cwd (project root, defaults to process.cwd()).

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

export default defineConfig({
  contracts: { glob: 'src/**/*.controller.ts', debounceMs: 500 },
  codegen: { outDir: 'src/generated', cwd: process.cwd() },

  validation: valibotAdapter,        // zodAdapter | valibotAdapter | arktypeAdapter
  extensions: [tanstackQuery({ import: '@tanstack/vue-query' })],

  forms: { enabled: true, watch: 'src/**/*.dto.ts' },

  serialization: 'json',             // 'json' (default) | 'superjson'
  driftGuard: true,                  // throw instead of silently overwriting on CLI↔module config drift

  // ── Inertia only (omit for a plain NestJS API) ──────────────────
  pages: { glob: 'resources/pages/**/*.tsx' },
  app: { moduleEntry: 'src/app.module.ts', tsconfig: 'tsconfig.json' },
});

Options

Prop

Type

Two more opt-in output blocks — openapi (OpenAPI 3.1 export) and mocks (MSW handler generation) — have their own pages. Each is skipped entirely unless you set enabled: true.

The fetcher is no longer a config concern. The generated api.ts is a createApi(fetcher) factory — you inject the fetcher (with its baseUrl, transport, and transformer) at runtime. See API client.

Drift guard

The CLI and the Nest module can each independently resolve the config and target the same codegen.outDir — if they genuinely disagree (e.g. one sets serialization: 'superjson', the other leaves the default 'json'), each run used to silently overwrite the other's api.ts shape. generate() now records which entry point ('cli' or 'module') produced a run, plus a hash of its resolved config, in the manifest (<outDir>/.codegen-manifest.json). When a run's entry point AND config hash both differ from the last recorded ones, it throws a DriftGuardError before writing anything, naming both entry points.

A normal config edit (same entry point) or two entry points that happen to resolve the same config both proceed as usual. The actual fix is a single source of truth — share one config object and import it into both nestjs-codegen.config.ts and forRoot(), as shown above — or set driftGuard: false on either config to opt out of the check entirely.

The tsconfig and path aliases

Discovery reads your tsconfig (app.tsconfig, else <cwd>/tsconfig.json) for its compilerOptions — nothing else. It never expands include/exclude into a file list, so a directory the codegen process cannot read (a docker bind mount a container chowned to its own UID, say) cannot break it.

What it does need is paths. A controller that inherits its routes from a factory — class WidgetsController extends createTableController(Widget) {} — is resolved through go-to-definition, so if the factory is imported through an alias (@/table/create-table-controller) and paths is missing, the factory does not resolve and every route that controller contributes is absent from the generated client. There is no error: tsc stays green, codegen exits 0, and the only signal is one warning per controller:

[nestjs-codegen/fast] WidgetsController in src/... extends createTableController(...) but
its callee does not resolve to a function or method declaration — it contributes NO routes
to the generated client.

A wall of those warnings means the tsconfig did not load, not that the controllers are wrong. The line immediately above them names the tsconfig and the underlying error. A consumer with no tsconfig at all is fine and stays silent — relative imports need no paths.

extends is followed, so paths may live in a base tsconfig. Mappings resolve the way tsc resolves them: against baseUrl when set, otherwise against the directory of the file that declared them.

The tsconfig and everything it extends are part of the freshness hash, so fixing one invalidates the artifact it produced and the next run regenerates rather than reporting up to date, skipped.

Controller factories and inherited routes

class WidgetsController extends createTableController({ entity: Widget }) {} contributes the routes of the class the factory returns. Discovery follows the whole prototype chain, the way Nest mounts it: the returned class, whatever that class extends — another factory call, or an ordinary base class — and so on to any depth. A method declared nearer the controller overrides a same-named one further up, so an override contributes one route, not two.

That is what makes an opt-in route work without any runtime trickery. Wrap the shared factory in a second one and declare the extra route as an ordinary decorated method:

export function createExportableTableController<E extends object>(options: TableOptions<E>) {
  class WithExport extends createTableController(options) {
    @Post('export')
    @HttpCode(HttpStatus.OK)
    async export(@Body() body: ExportRequestDto): Promise<ExportResultDto> { … }
  }
  return WithExport;
}

@Controller('widgets')
class WidgetsController extends createExportableTableController({ entity: Widget }) {} // has export

@Controller('gadgets')
class GadgetsController extends createTableController({ entity: Gadget }) {} // does not

Which factory a controller extends decides which routes it gets, and every route stays a literal decorator that the static scan can see.

The alternative — declaring the handler undecorated and mounting it imperatively (if (options.export) Post('export')(proto, 'export', descriptor)) — serves fine at runtime and is invisible to codegen: there is no decorator in the source to find, so the route is simply absent from the generated client, with no warning. Reach for the wrapping factory instead.

Types on an inherited route resolve in the file that declares the handler — the factory's file — so a @Body()/@Query() DTO or a return type the factory imports resolves normally even though the controller's own file never names it.

Multi-scope discovery

Experimental — not yet wired. scopes is currently parsed and validated into the resolved config, but the discovery/generate pipeline does not consume it yet (no code reads .scopes), so setting it has no effect today. It's documented here as the intended shape; until it's wired, generate each surface with a separate run/config (distinct contracts.glob + outDir). Track this before relying on it in CI.

Most projects scan a single controller set via contracts.glob. When you need to generate from several distinct surfaces — say a public API and an internal admin API that live in different directories and mount under different route prefixes — use scopes. It's a record of named scopes, each a ScopeConfig ({ glob, prefix? }):

nestjs-codegen.config.ts
export default defineConfig({
  validation: zodAdapter,
  codegen: { outDir: 'src/generated' },

  scopes: {
    public: { glob: 'src/public/**/*.controller.ts', prefix: '/api' },
    admin: { glob: 'src/admin/**/*.controller.ts', prefix: '/admin' },
  },
});

Prop

Type

On this page