Getting Started
The fastest way to get Inertia.js running in NestJS is two steps: install the packages, then run nestjs-inertia init. The init command scaffolds all files and auto-patches your existing app.module.ts and main.ts — no manual wiring needed. Most projects are done in under three minutes.
Quickstart
Section titled “Quickstart”Step 1 — Install
Section titled “Step 1 — Install”Install the five packages that make up the suite, then add the Inertia client adapter for your frontend framework.
pnpm add @dudousxd/nestjs-inertia @dudousxd/nestjs-inertia-vite @dudousxd/nestjs-inertia-clientpnpm add -D @dudousxd/nestjs-inertia-codegen @dudousxd/nestjs-inertia-testingThen install your framework’s Inertia adapter and Vite plugin:
pnpm add @inertiajs/react react react-dompnpm add -D @types/react @types/react-dom @vitejs/plugin-reactpnpm add @inertiajs/vue3 vuepnpm add -D @vitejs/plugin-vuepnpm add @inertiajs/svelte sveltepnpm add -D @sveltejs/vite-plugin-svelteStep 2 — Run nestjs-inertia init
Section titled “Step 2 — Run nestjs-inertia init”pnpm exec nestjs-inertia initThis command scaffolds your entire Inertia setup in one shot. It:
- Detects your frontend framework from
package.json(React, Vue 3, or Svelte 5). If none is found it asks interactively. - Detects your template engine — Handlebars, EJS, Pug, Liquid, or plain HTML.
- Creates scaffold files — skips any that already exist (fully idempotent). If
vite.config.tsexists but doesn’t reference thenestInertiaplugin, a warning is printed instead of overwriting your config:
| File | What it is |
|---|---|
nestjs-inertia.config.ts | Codegen config with a framework-specific page glob |
nestjs-inertia.d.ts | TypeScript module augmentation for generated types |
inertia/index.html (or .hbs/.ejs/.pug) | HTML shell with the Inertia page script tag |
vite.config.ts | Vite config with the correct framework plugin |
inertia/app.tsx (or .ts) | Frontend entry point bootstrapping createInertiaApp |
inertia/pages/Home.tsx (or .vue/.svelte) | Sample page component |
src/home.controller.ts | Sample NestJS controller with @Inertia('Home') |
- Intelligently patches existing files — each patch is only applied if the content is not already present:
src/app.module.ts— addsInertiaModule.forRoot(...)toimportsandHomeControllertocontrollerssrc/main.ts— inserts thesetupInertiaVite(app, {...})call right afterNestFactory.create.gitignore— appends.nestjs-inertia/if not already presentpackage.jsonscripts — addsbuild:clientandbuild:ssronly if not already defined
Expected output (fresh project):
nestjs-inertia init
Detected: React + plain HTML
Scaffold files ✓ nestjs-inertia.config.ts (created) ✓ nestjs-inertia.d.ts (created) ✓ inertia/index.html (created) ✓ vite.config.ts (created) ✓ inertia/app.tsx (created) ✓ inertia/pages/Home.tsx (created) ✓ src/home.controller.ts (created)
Patch existing files ✓ src/app.module.ts (added InertiaModule.forRoot) ✓ src/app.module.ts (added HomeController to controllers) ✓ src/main.ts (added setupInertiaVite after NestFactory.create) ✓ .gitignore (added .nestjs-inertia/) ✓ package.json (added build:client script) ✓ package.json (added build:ssr script)
Install dependencies ✓ @inertiajs/react, react, react-dom (installed)
✓ Setup complete! Run: nest start --watchRe-running init on an existing project skips files that already exist and skips patches already applied — it is fully idempotent:
Scaffold files → nestjs-inertia.config.ts (already exists, skipped) → inertia/index.html (already exists, skipped) → vite.config.ts (already exists, skipped) ...
Patch existing files → src/app.module.ts (InertiaModule already registered, skipped) → src/main.ts (setupInertiaVite already present, skipped) → .gitignore (already contains .nestjs-inertia/, skipped) → package.json (build:client already defined, skipped)Done. Run nest start --watch and open http://localhost:3000.
Manual setup (if auto-patching fails)
Section titled “Manual setup (if auto-patching fails)”If init cannot find or parse your app.module.ts or main.ts (e.g. non-standard structure), it prints a warning with the exact snippet to add manually.
src/app.module.ts — register InertiaModule and HomeController:
import { InertiaModule } from '@dudousxd/nestjs-inertia';import { HomeController } from './home.controller.js';
@Module({ imports: [ InertiaModule.forRoot({ rootView: 'inertia/index.html', }), ], controllers: [HomeController],})export class AppModule {}src/main.ts — wire Vite after NestFactory.create:
const app = await NestFactory.create(AppModule);
const { setupInertiaVite } = await import('@dudousxd/nestjs-inertia-vite');await setupInertiaVite(app, { mode: process.env.NODE_ENV ?? 'development', root: 'inertia', publicDir: 'dist/inertia/client', outDir: 'dist/inertia',});
await app.listen(3000);The HTML shell and its directives
Section titled “The HTML shell and its directives”The shell file created by init uses three special directives. These are the key to how nestjs-inertia works.
<!DOCTYPE html><html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>My App</title> @inertiaHead </head> <body> @inertia @vite('inertia/app.tsx') </body></html>| Directive | What it emits |
|---|---|
@inertia | <div id="app"></div> plus <script type="application/json">{page data}</script>. The page JSON is XSS-safe escaped. |
@inertiaHead | SSR <head> tags (title, meta) when SSR is enabled. Empty string in CSR-only mode. |
@vite('entry') | In dev: <script src="/@vite/client"> (HMR) + React Refresh preamble + entry script. In prod: reads the Vite manifest and emits the hashed asset URL + CSS link. |
You write the <head> with your app’s meta tags, fonts, and favicon. The lib handles everything else.
Going deeper
Section titled “Going deeper”The sections below cover the full configuration surface for each piece. They are optional reading — the quickstart above is all you need to get running.
InertiaModule.forRoot() options
Section titled “InertiaModule.forRoot() options”| Option | Type | Required | Description |
|---|---|---|---|
version | string | () => string | No | Asset version — auto-computed from Vite manifest if omitted (SHA-1 in prod, UUID in dev) |
rootView | string | (ctx) => string | Yes | HTML shell — file path or render function |
ssr | { enabled, bundlePath, throwOnError? } | No | SSR bundle configuration |
share | (req) => Promise<Record> | No | Per-request shared props injected into every page |
csrf | { secret, cookieName, tokenContext? } | No | CSRF cookie options |
codegen | { enabled } | No | Enable the dev-mode file watcher for auto-codegen |
For the full options reference see the core package docs.
Version auto-detection: When version is omitted, the lib automatically computes it from the Vite manifest in production (SHA-1 hash) or generates a random UUID in development. You only need to set version explicitly if you have a custom versioning strategy.
Adding a typed API endpoint
Section titled “Adding a typed API endpoint”The codegen reads types directly from standard NestJS decorators — no extra schemas or contracts needed. Use the DTOs and decorators you already have:
import { Controller, Get, Query } from '@nestjs/common';import { ApiResponse } from '@nestjs/swagger';import { As } from '@dudousxd/nestjs-inertia-client';import { IsNumber, IsOptional, IsString } from 'class-validator';import { Type } from 'class-transformer';
class ListPostsQuery { @IsOptional() @IsNumber() @Type(() => Number) page?: number;}
class PostDto { @IsString() id: string;
@IsString() title: string;
@IsString() publishedAt: string;}
@Controller('/api/posts')@As('posts')export class PostsController { @Get() @ApiResponse({ type: [PostDto] }) list(@Query() query: ListPostsQuery) { return [ { id: '1', title: 'Hello World', publishedAt: '2026-01-01' }, ]; }}The codegen reads:
@Controller('/api/posts')+@Get()→ HTTP method + path@Query() query: ListPostsQuery→ query type from the DTO class@ApiResponse({ type: [PostDto] })→ response type@As('posts')→ class namespace; method namelist→ route nameposts.list
No Zod, no defineContract, no @ApplyContract. Just standard NestJS.
Advanced: If you prefer explicit Zod schemas (runtime validation + type extraction), see the defineContract + @ApplyContract docs. Both paths work — DTOs are zero-config, Zod is explicit.
Codegen
Section titled “Codegen”Codegen runs automatically — no config needed. When @dudousxd/nestjs-inertia-codegen is installed and a nestjs-inertia.config.ts is present, the watcher starts on nest start --watch and regenerates types on every controller/page change. It’s disabled automatically in production.
On startup the watcher emits files into .nestjs-inertia/ at the project root:
your-project/├── .nestjs-inertia/│ ├── pages.d.ts ← typed page component map│ ├── routes.ts ← Route.* and Path.* typed route constants│ ├── api.ts ← typed API surface from controller DTOs + @ApiResponse│ └── index.d.ts ← barrel re-exportFor CI or Docker builds, run codegen explicitly before TypeScript compilation:
pnpm exec nestjs-inertia codegenpnpm exec tsc --noEmitpnpm run buildSee the Codegen guide for full configuration and generated file reference.
Typed <Link> component
Section titled “Typed <Link> component”Wire the route resolver in your frontend entry, then use <Link> in any page component.
// inertia/app.tsx — add InertiaRouteProviderimport { InertiaRouteProvider } from '@dudousxd/nestjs-inertia-client/react';import { route } from '../.nestjs-inertia/routes.js';
// Inside setup:createRoot(el!).render( <InertiaRouteProvider routes={route}> <App {...props} /> </InertiaRouteProvider>,);// In any page componentimport { Link } from '@dudousxd/nestjs-inertia-client/react';
<Link route="posts.list">All Posts</Link><Link route="posts.show" routeParams={{ id: '42' }}>Post #42</Link><Link route="posts.list" query={{ page: 2 }}>Page 2</Link>// inertia/app.ts — provide the route resolverimport { INERTIA_ROUTES_KEY } from '@dudousxd/nestjs-inertia-client/vue';import { route } from '../.nestjs-inertia/routes.js';
// Inside setup:app.provide(INERTIA_ROUTES_KEY, route);<!-- In any page component --><script setup lang="ts">import { Link } from '@dudousxd/nestjs-inertia-client/vue';</script><template> <Link route="posts.list">All Posts</Link> <Link route="posts.show" :route-params="{ id: '42' }">Post #42</Link></template>// inertia/app.ts — pass resolver as Svelte contextimport { route } from '../.nestjs-inertia/routes.js';const appContext = new Map([['inertia-routes', route]]);// In setup: mount(App, { target: el, props, context: appContext });<!-- In any page component --><script lang="ts">import { Link } from '@dudousxd/nestjs-inertia-client/svelte';</script><Link route="posts.list">All Posts</Link>See the Typed Link guide for full <Link> props and programmatic navigation.
Type helpers
Section titled “Type helpers”The generated api.ts exports a Route namespace with helper types that let you extract request/response shapes for any named endpoint.
import type { Route } from '../.nestjs-inertia/api';
type PostList = Route.Response<'posts.list'>;type PostListQuery = Route.Query<'posts.list'>;type PostShowParams = Route.Params<'posts.show'>;These types are derived entirely from the Zod schemas passed to defineContract — they stay in sync automatically.
See the Typed Client guide for createFetcher and SSR hydration.
Testing
Section titled “Testing”nestjs-inertia-testing provides expectInertia() — a chainable assertion helper for Inertia responses.
import { Test } from '@nestjs/testing';import { INestApplication } from '@nestjs/common';import request from 'supertest';import { expectInertia } from '@dudousxd/nestjs-inertia-testing';import { AppModule } from './app.module.js';
describe('HomeController', () => { let app: INestApplication;
beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleRef.createNestApplication(); await app.init(); });
afterAll(() => app.close());
it('renders the Home component', async () => { const res = await request(app.getHttpServer()) .get('/') .set('X-Inertia', 'true') .set('X-Inertia-Version', '1');
expectInertia(res).toRenderComponent('Home'); });});| Assertion | Description |
|---|---|
.toRenderComponent(name) | Asserts the component field matches the expected name |
.toHaveProp(path, value) | Deep-equality check on a dot-notation prop path |
.toHaveProps(object) | Partial deep-equality on the entire props object |
.toHaveSharedProp(path, value) | Checks a prop injected via share or req.inertia.share() |
See the Testing guide for the full API, TestingModule harness, and faking shared props.
Production build
Section titled “Production build”The init command adds build:client and build:ssr scripts to package.json. The full production build sequence:
pnpm run build:client # vite build — emits client bundle + manifestpnpm run build:ssr # vite build --ssr — emits SSR bundlenest build # compiles TypeScript to dist/NODE_ENV=production node dist/main.jsIn production setupInertiaVite switches from dev-server middleware to static-file serving from publicDir. The computeAssetVersion factory (shown in the forRoot section above) reads the real manifest and returns a stable hash.
Project structure after init
Section titled “Project structure after init”my-nestjs-app/├── src/│ ├── main.ts ← NestJS bootstrap + setupInertiaVite (patched by init)│ ├── app.module.ts ← InertiaModule.forRoot(...) (patched by init)│ └── home.controller.ts ← @Inertia('Home') handler (created by init)├── inertia/│ ├── app.tsx ← createInertiaApp entry (created by init)│ ├── index.html ← HTML shell with Inertia page script (created by init)│ └── pages/│ └── Home.tsx ← sample page (created by init)├── .nestjs-inertia/ ← generated (gitignored)│ ├── pages.d.ts│ ├── routes.ts│ └── api.ts├── nestjs-inertia.config.ts ← codegen config (created by init)├── nestjs-inertia.d.ts ← ambient declaration (created by init)├── vite.config.ts ← (created by init)└── package.jsonNext steps
Section titled “Next steps”- Guides: Codegen — full codegen configuration, CI integration, and generated file reference
- Guides: Typed Client —
createFetcher, SSR hydration, and client-side contract usage - Guides: Typed Link —
<Link>props,routeParamsinference, and programmatic navigation - Guides: Testing —
expectInertiafull API,TestingModuleharness, and faking shared props - Guides: Multi-app —
InertiaModule.forFeature(...)for multiple independent frontends in one process - Packages: Core — full
InertiaModule.forRootoptions,InertiaServiceAPI, and partial-reload markers - Packages: Vite —
nestInertiaplugin options,setupInertiaViteoptions, and SSR build config - Recipes: Auth Redirect —
InertiaAuthGuardpattern for 302 vs. 409 redirect on unauthenticated Inertia requests - Recipes: Not Found Filter — exception filter that renders an Inertia page for 404s instead of JSON