Codegen
@dudousxd/nestjs-inertia-codegen provides a CLI that statically analyses your NestJS application and emits TypeScript type files so that your frontend and any API client are fully type-safe.
Install
Section titled “Install”pnpm add -D @dudousxd/nestjs-inertia-codegenConfiguration
Section titled “Configuration”Create nestjs-inertia.config.ts at your project root:
import { defineConfig } from '@dudousxd/nestjs-inertia-codegen';
export default defineConfig({ pages: { // Glob pattern for Inertia page components glob: 'inertia/pages/**/*.tsx', // Named export that holds the page's prop type propsExport: 'ComponentProps', }, app: { // Path to the NestJS AppModule (used to discover controllers) moduleEntry: './src/app.module.ts', }, contracts: { // Glob for controller files watched in auto/manual watch mode glob: 'src/**/*.controller.ts', // Debounce before re-running contract discovery on file change debounceMs: 500, }, // Optional: where to emit generated files (default: .nestjs-inertia/) outDir: '.nestjs-inertia',});Running
Section titled “Running”pnpm nestjs-inertia codegenOr add a script to package.json:
{ "scripts": { "codegen": "nestjs-inertia codegen" }}Output structure
Section titled “Output structure”.nestjs-inertia/ pages.d.ts — union type of all known Inertia page component names routes.ts — typed route helpers derived from @Inertia() metadata api.ts — typed client API surface derived from @ApplyContract() metadatapages.d.ts
Section titled “pages.d.ts”export type InertiaPages = 'Dashboard' | 'Users' | 'Settings';Use this type to constrain the component argument wherever you construct Inertia responses programmatically.
routes.ts
Section titled “routes.ts”export const routes = { dashboard: () => '/dashboard', users: { list: () => '/api/users' },} as const;api.ts
Section titled “api.ts”export interface InertiaApi { 'users.list': { method: 'GET'; url: '/api/users'; query: { active?: boolean }; response: Array<{ id: string; name: string }>; };}This file is consumed by @dudousxd/nestjs-inertia-client’s createFetcher to produce a fully typed client.
Using class-validator DTOs (no defineContract needed)
Section titled “Using class-validator DTOs (no defineContract needed)”For standard NestJS projects that already use class-validator DTOs and @nestjs/swagger, you get typed API generation without writing defineContract at all. The codegen reads types directly from:
@Body() body: CreatePostDto→ body type@Query() query: ListPostsQuery→ query type@Param('id') id: string→ params type@ApiResponse({ type: [PostDto] })or@ApiResponse({ type: PostDto })→ response type- Return type annotation (
Promise<PostDto[]>) → response fallback
import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';import { ApiResponse } from '@nestjs/swagger';
class ListPostsQuery { page?: number;}
class PostDto { id: string; title: string;}
class CreatePostBody { title: string; content: string;}
@Controller('/api/posts')export class PostsController { @Get() @ApiResponse({ type: [PostDto] }) list(@Query() query: ListPostsQuery): Promise<PostDto[]> { return this.postsService.list(query); }
@Post() @ApiResponse({ type: PostDto }) create(@Body() body: CreatePostBody): Promise<PostDto> { return this.postsService.create(body); }
@Get(':id') @ApiResponse({ type: PostDto }) show(@Param('id') id: string): Promise<PostDto> { return this.postsService.findOne(id); }}After running pnpm codegen, the api.ts file will contain fully typed entries for all three methods — no defineContract, no Zod schemas required.
Priority chain
Section titled “Priority chain”When building contract info for a method the codegen applies this priority:
@ApplyContract(defineContract({...}))— Zod schemas win. When present, DTO extraction is skipped entirely for that method.- Parameter decorators —
@Body(),@Query(),@Param('name')contribute body, query, and params types from the DTO class. @ApiResponse({ type: ... })— provides the response type.- Return type annotation —
list(): Promise<PostDto[]>is used as a response fallback when@ApiResponseis absent. - Nothing —
unknown.
Notes and limitations
Section titled “Notes and limitations”@Query('key')for individual string query params is ignored (only@Query()with no argument pointing to a DTO class is extracted).- DTO classes must be declared in the same file as the controller for the fast AST path to resolve them. Imported DTOs fall back to the class name string.
- Recursive class resolution is limited to depth 3 to prevent infinite loops on circular references.
Dateproperties are emitted asstring(common REST API convention).defineContractwith Zod is the advanced path for teams that prefer explicit, validated schemas. DTOs are the zero-boilerplate path for teams already using class-validator.
@ApplyContract example
Section titled “@ApplyContract example”import { Controller, Get } from '@nestjs/common';import { ApplyContract, defineContract } from '@dudousxd/nestjs-inertia-client';import { z } from 'zod';
const ListUsers = defineContract({ query: z.object({ active: z.boolean().optional() }), response: z.array(z.object({ id: z.string(), name: z.string() })),});
@Controller()export class UsersController { @Get('/api/users') @ApplyContract(ListUsers) list() { return [ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }, ]; }}Use standard NestJS decorators (@Get, @Post, @Put, @Patch, @Delete) as the routing source-of-truth.
@ApplyContract only attaches the contract metadata — it no longer sets the HTTP method or path.
After running pnpm codegen, the auto-derived users.list contract appears in api.ts.
How the API name is chosen
Section titled “How the API name is chosen”The API name is composed from a class portion and a method portion, joined with a dot:
- Class portion: class-level
@As(...)value if present, otherwise the class name with theControllersuffix stripped and first letter lowercased. - Method portion: method-level
@As(...)value if present, otherwise the method name. - Final name:
${classPortion}.${methodPortion}
Class-level @As | Method-level @As | Derived name |
|---|---|---|
| absent | absent | <classNameStripped>.<methodName> (default) |
@As('crew') | absent | crew.<methodName> |
| absent | @As('top10') | <classNameStripped>.top10 |
@As('crew') | @As('directory.fetch') | crew.directory.fetch |
@As('crew.admin') | @As('top10') | crew.admin.top10 |
Use dot-notation in the final name to produce a nested path under api.*.*:
| Final name | Generated accessor |
|---|---|
'users.list' | api.users.list |
'users.show' | api.users.show |
'adminUsers.create' | api.adminUsers.create |
'crew.admin.top10' | api.crew.admin.top10 |
// UsersController.list → api.users.list.queryOptions(...)const { data } = useQuery(api.users.list.queryOptions({ active: true }));Overriding the name with @As
Section titled “Overriding the name with @As”@As can be applied at the class level, method level, or both. When both are present they compose: the class value becomes the class portion and the method value becomes the method portion.
import { As } from '@dudousxd/nestjs-inertia-client';
// Class-level @As sets the class portion for every method in the class@Controller('/api/v1/crew')@As('crew')class CrewController { @Get() @ApplyContract(ListCrew) list() { ... } // → 'crew.list'
@Get('/top') @ApplyContract(TopCrew) @As('top10') // → 'crew.top10' top() { ... }}
// Class @As can be multi-segment@Controller('/api/v1/admin/crew')@As('crew.admin')class CrewAdminController { @Get() @ApplyContract(ListCrew) @As('top10') // → 'crew.admin.top10' list() { ... }}Valid name format
Section titled “Valid name format”Each dot-separated segment must be a camelCase identifier: it must start with a lowercase letter and contain only letters and digits (no hyphens, underscores, spaces, or uppercase first letters). Codegen validates this at emit time and throws a descriptive error with a suggested fix.
- Valid:
'users.list','admin.userActions.create','health' - Invalid (throws):
'user-post.list'→ suggest'userPost.list','user_post.list'→ suggest'userPost.list','User.list'→ suggest'user.list'
Type helpers
Section titled “Type helpers”The generated api.ts exports two namespaces of type helpers for accessing request and response shapes without importing the full client.
Route.* — by contract name
Section titled “Route.* — by contract name”import type { Route } from '.nestjs-inertia/api.js';
// Resolved type of the response field for users.listtype UserList = Route.Response<'users.list'>;
// Full request shapetype CreateUserRequest = Route.Request<'users.create'>;// → { body: { name: string; email: string }; query: never; params: never }All helpers: Route.Response<K>, Route.Body<K>, Route.Query<K>, Route.Params<K>, Route.Error<K>, Route.Request<K>.
Path.* — by HTTP method + URL pattern
Section titled “Path.* — by HTTP method + URL pattern”import type { Path } from '.nestjs-inertia/api.js';
type ListResponse = Path.Response<'GET', '/api/users'>;type CreateBody = Path.Body<'POST', '/api/users'>;All helpers: Path.Response<M, U>, Path.Body<M, U>, Path.Query<M, U>, Path.Params<M, U>, Path.Error<M, U>.
Auto-watch (recommended)
Section titled “Auto-watch (recommended)”When the codegen package is installed in your project, InertiaModule automatically starts the codegen watcher when your app bootstraps in dev mode. Just run your usual nest start --watch — the generated files appear under .nestjs-inertia/ and update on every controller/page save. No extra command, no extra terminal.
Auto-watch starts when all of these are true:
NODE_ENV !== 'production'@dudousxd/nestjs-inertia-codegenis installednestjs-inertia.config.tsis present at the project root- The kill-switch env
NESTJS_INERTIA_DISABLE_AUTO_CODEGENis not set to'1'
Running the CLI watcher manually
Section titled “Running the CLI watcher manually”You can also run pnpm nestjs-inertia codegen --watch in a separate terminal if you prefer explicit control. When both the auto-watcher AND the CLI watcher are running, only one will hold the lock and generate files; the other becomes a no-op and logs a warning. Stale locks from crashed processes are detected via PID-liveness check and overwritten automatically — so a crash never leaves you stuck without codegen.
Disabling auto-watch
Section titled “Disabling auto-watch”InertiaModule.forRoot({ codegen: { enabled: false },});Or set NESTJS_INERTIA_DISABLE_AUTO_CODEGEN=1 in your shell.
Optional nest-cli.json snippet
Section titled “Optional nest-cli.json snippet”Only needed if your server bundle imports generated files (e.g. route helpers from .nestjs-inertia/routes.ts). This tells the NestJS compiler to copy and watch the output directory:
{ "compilerOptions": { "assets": [".nestjs-inertia/**/*"], "watchAssets": true }}Manual watch mode (CLI)
Section titled “Manual watch mode (CLI)”pnpm nestjs-inertia codegen --watchReruns whenever a controller or page file changes. When using this alongside nest start --watch, the auto-bootstrap watcher detects the held lock and becomes a no-op automatically.
Generated file management
Section titled “Generated file management”Add the output directory to version control so that CI has types without running the CLI. Alternatively, add a postinstall script to generate on install and add .nestjs-inertia/ to .gitignore.