nestjs-inertia
Pages, shared props, and Inertia router navigation.
When you build a NestJS + Inertia app, register the inertia extension and point
pages.glob/app.moduleEntry at your app. The codegen then additionally:
- discovers your Inertia pages and emits
pages.d.ts+components.json; - reads shared props from
InertiaModule.forRoot({ share }); - emits a typed
navigate()helper backed by the Inertiarouter.
pnpm add -D @dudousxd/nestjs-inertia-codegen-extensionimport { defineConfig } from '@dudousxd/nestjs-codegen';
import { nestjsInertiaCodegen } from '@dudousxd/nestjs-inertia-codegen-extension';
export default defineConfig({
pages: { glob: 'resources/pages/**/*.tsx' },
contracts: { glob: 'src/**/*.controller.ts' },
app: { moduleEntry: 'src/app.module.ts' },
codegen: { outDir: 'resources/generated' },
extensions: [nestjsInertiaCodegen()],
});Typed navigation
import { navigate } from '../generated/api';
navigate('users.show', { params: { id: '42' } }); // typed route + paramsWhen nestjsInertiaCodegen() is registered, the generated api.ts imports router
from @inertiajs/react for this helper; without the extension it isn't imported at all.
The pages typing augments @dudousxd/nestjs-inertia's InertiaPages interface, so
inertia.render('Users/Index', props) is checked against the discovered page props.
Shared props
By default, the codegen statically analyzes InertiaModule.forRoot({ share }) in
app.moduleEntry and augments InertiaSharedProps (declaration-merged into
@dudousxd/nestjs-inertia) directly in pages.d.ts. That works as long as share is a
plain function whose return type can be resolved from that one call site.
When shared props are registered per-request instead — e.g. req.inertia.share(...) in
middleware, which the static analysis above can't see — pass shared to
nestjsInertiaCodegen() to declare the source explicitly:
extensions: [
nestjsInertiaCodegen({
shared: { module: 'src/middleware/inertia-share', export: 'buildSharedProps', kind: 'function' },
}),
],This emits a standalone shared.ts re-exporting a type-only InertiaSharedProps, so no
runtime import of server code leaks into the frontend build:
kind: 'function'— the export is a factory (e.g. theshare()callback); the type isAwaited<ReturnType<typeof theExport>>.kind: 'type'— the export IS the shape, atypealias orinterface.
Codegen throws an actionable error if module doesn't resolve to a file, or export doesn't
exist in it (or doesn't look like the declared kind) — a broken shared.ts import would
otherwise surface far from its cause, as a confusing type error in the frontend build.
Excluding pages from the API prefix
If you call setGlobalPrefix('api', { exclude }), a hand-maintained exclude list is easy to
let drift out of sync with your actual Inertia pages. Pass pageExcludes: true to generate it
instead:
extensions: [nestjsInertiaCodegen({ pageExcludes: true })],This scans contracts.glob for @Inertia-decorated, HTTP-method-decorated controller
methods and emits a framework-free page-excludes.ts:
export const inertiaPageExcludes = [
{ path: '/users', method: 'GET' },
{ path: '/users/:id', method: 'GET' },
] as const;import { inertiaPageExcludes } from './generated/page-excludes';
app.setGlobalPrefix('api', { exclude: inertiaPageExcludes });Throws if zero @Inertia pages are found under contracts.glob — an empty list almost
always means the glob doesn't reach your view controllers.
Both shared and pageExcludes are additive and independently opt-in. Calling
nestjsInertiaCodegen() with no arguments is unchanged — no extra files, only the
navigate() header contribution described above.