npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dudousxd/nestjs-inertia

v1.5.0

Published

Inertia.js adapter for NestJS — core protocol and module.

Downloads

3,984

Readme

@dudousxd/nestjs-inertia

Inertia.js v3 adapter for NestJS — Express + Fastify, multi-app via forFeature, 4 template engines, CSRF protection, full Inertia v3 protocol parity.

npm license

Install

pnpm add @dudousxd/nestjs-inertia

# Pick your HTTP platform
pnpm add express @types/express          # Express adapter (default)
# OR
pnpm add fastify @nestjs/platform-fastify @fastify/cookie  # Fastify adapter

Quick start

// app.module.ts
import { Module } from '@nestjs/common';
import { InertiaModule } from '@dudousxd/nestjs-inertia';

@Module({
  imports: [
    InertiaModule.forRoot({
      version: () => process.env.ASSET_VERSION ?? 'dev',
      rootView: 'inertia/root.html',
      share: async (req) => ({ auth: req.user ?? null }),
    }),
  ],
})
export class AppModule {}

inertia/root.html:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <title>My App</title>
  @inertiaHead
  @vite('app/client.tsx')
  @viteRefresh
</head>
<body>
  @inertia
</body>
</html>

Controllers

Two equivalent patterns coexist:

import { Controller, Get, Req } from '@nestjs/common';
import { Inertia } from '@dudousxd/nestjs-inertia';
import type { Request } from 'express';

@Controller()
export class HomeController {
  // Decorator pattern (idiomatic for new code)
  @Get('/')
  @Inertia('Home')
  show() {
    return { hello: 'world' };
  }

  // Imperative pattern (use when you need fine control)
  @Get('/crew')
  async list(@Req() req: Request) {
    await req.inertia
      .share({ flash: req.session?.flash ?? {} })
      .render('Crew', { crew: await this.svc.list() });
  }
}

Async config

InertiaModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (cfg: ConfigService) => ({
    version: cfg.get('ASSET_VERSION'),
    rootView: 'inertia/root.html',
  }),
});

Three paths: useFactory + inject (most common), useClass, useExisting.

Multi-app with forFeature

Host two or more Inertia apps in the same NestJS process — each with its own Vite entry, shell, version, share, SSR bundle. Useful for admin panels, multi-tenant white-label, or migrations between frontend stacks.

@Module({
  imports: [
    InertiaModule.forRoot({                  // main app
      vite: { entry: 'app/client.tsx' },
      rootView: 'inertia/root.html',
      share: req => ({ auth: req.user }),
    }),
    InertiaModule.forFeature({               // admin app
      scope: 'admin',
      vite: { entry: 'admin/client.tsx' },
      rootView: 'inertia/admin-root.html',
      share: req => ({ admin: req.adminContext }),
    }),
  ],
})
export class AppModule {}

Select scope per controller / method with @UseInertia('scope'):

@Controller('admin')
@UseInertia('admin')
export class AdminDashboardController {
  @Get('/')
  @Inertia('AdminDashboard')
  show() { return { stats: ... }; }
}

forFeatureAsync works the same as forRootAsync (useFactory/useClass/useExisting). Reserved scope: 'default' is owned by forRoot().

Template engines

rootView accepts .html (own parser) plus .hbs / .ejs / .pug / .liquid if the engine package is installed:

pnpm add handlebars                    # or: ejs, pug, liquidjs
InertiaModule.forRoot({
  rootView: 'inertia/root.hbs',         // auto-detects Handlebars
});

Each engine sees locals { page, inertia, inertiaHead, vite, viteRefresh, asset }. Use the engine's own escape rules (e.g., {{{inertia}}} triple-stache in Handlebars, <%- inertia %> in EJS, != inertia in Pug). The @inertia/@vite/@asset directives are also processed on the engine's output, so you can mix and match.

CSRF

import { CsrfCookieInterceptor, CsrfGuard } from '@dudousxd/nestjs-inertia';

// Global cookie writer
app.useGlobalInterceptors(new CsrfCookieInterceptor({ secret: process.env.CSRF_SECRET }));

// Per-route validation
@UseGuards(new CsrfGuard({ secret: process.env.CSRF_SECRET }))
@Post('/profile')
async update() { ... }

Cookie name XSRF-TOKEN, header name X-XSRF-TOKEN — both match the Inertia client convention. Signed via HMAC-SHA256. Requires cookie-parser (Express) or @fastify/cookie (Fastify) as peer dep.

Fastify

import { FastifyAdapter } from '@nestjs/platform-fastify';

const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());

Full feature parity with Express: middleware, decorator, all interceptors, guards, filters, shell directives, SSR, FlashStore. request.inertia is wired via decorateRequest + onRequest hook automatically when the FastifyAdapter is detected.

Prop markers

import { Inertia } from '@dudousxd/nestjs-inertia';

return {
  user: Inertia.always(() => currentUser()),
  stats: Inertia.optional(() => heavyStatsCalculation()),
  activity: Inertia.defer(() => activityFeed(), 'secondary'),
  rows: Inertia.merge(() => paginated(p), { matchOn: 'id', deep: false }),
  csrfToken: Inertia.once(() => generateToken()),
};
  • always — resolves on every render (including partial reloads)
  • optional — resolves only when listed in X-Inertia-Partial-Data
  • defer — listed in page.deferredProps; client v2 dispatches a follow-up request
  • merge — resolves + marks as merge target (client appends/replaces)
  • once — resolves on first visit, cached until X-Inertia-Reset-Once lists the key
  • lazy — alias for optional (v1 compat)

Auto-included infrastructure

forRoot() installs:

  • InertiaMiddleware (Express) or FastifyInertiaPlugin (Fastify) — req.inertia available everywhere
  • MethodSpoofMiddleware (POST + multipart + _method=PUT/PATCH/DELETE)
  • RedirectInterceptor (302 → 303 upgrade on PUT/PATCH/DELETE Inertia requests)
  • InertiaRenderInterceptor (handles @Inertia('Page') decorator)
  • InertiaScopeSwitcherInterceptor (handles @UseInertia('scope'))

Disable via knobs:

InertiaModule.forRoot({
  methodSpoofing: false,
  autoUpgrade303: false,
});

Opt-in utilities

import {
  InertiaAuthGuard,
  InertiaNotFoundFilter,
  ErrorBagInterceptor,
} from '@dudousxd/nestjs-inertia';

// Auth guard — applies per controller / handler
@UseGuards(new InertiaAuthGuard({ signInUrl: '/signin', allowList: ['/signin/*'] }))

// Not-found filter — register globally
app.useGlobalFilters(new InertiaNotFoundFilter({ apiPrefix: '/api', component: 'NotFound' }));

// Error bag interceptor — opt-in per route
@UseInterceptors(ErrorBagInterceptor)

FlashStore (errors)

NestJS has no session of its own. Plug an adapter:

import type { FlashStore } from '@dudousxd/nestjs-inertia';

class ExpressSessionFlashStore implements FlashStore {
  read(req) {
    return (req as Request).session?.flash?.errors ?? {};
  }
}

InertiaModule.forRoot({
  flashStore: new ExpressSessionFlashStore(),
});

SSR

InertiaModule.forRoot({
  ssr: {
    enabled: process.env.NODE_ENV === 'production',
    bundlePath: 'dist/inertia/ssr/ssr.mjs',
    throwOnError: false,
  },
});

Bundle must export default { render(page) } or named render(page) returning { head: string[], body: string }.

Codegen auto-watch (dev mode)

When @dudousxd/nestjs-inertia-codegen is installed and a nestjs-inertia.config.ts config file is present, InertiaModule automatically starts the codegen file watcher when your app bootstraps — so nest start --watch is the only command you need in dev mode. Generated files appear under .nestjs-inertia/ and update on every controller or page save. No extra command, no extra terminal.

Auto-watch starts when all of these are true:

  1. NODE_ENV !== 'production'
  2. @dudousxd/nestjs-inertia-codegen is installed (peer-optional — silently skipped if absent)
  3. nestjs-inertia.config.ts is present at the project root
  4. NESTJS_INERTIA_DISABLE_AUTO_CODEGEN is not set to '1'

Running the CLI watcher manually: pnpm nestjs-inertia codegen --watch in a separate terminal gives you explicit control. When both the auto-watcher and the CLI watcher run at the same time, only one holds the lock and generates files; the other logs a warning and becomes a no-op. Stale locks from crashed processes are detected via PID-liveness check and overwritten automatically.

No files generated? Check that the app finished booting. The auto-watch starts in onApplicationBootstrap — it runs only after every module finishes initializing. A boot that stalls mid-init (a hung provider factory, an unreachable dependency like a dead local SQS/Redis container, etc.) produces no codegen output and no codegen error. The log tells you which case you're in:

[InertiaModule] Codegen auto-watch will start after application bootstrap. ...   ← logged early (module init)
[InertiaModule] Codegen auto-watch started (dev mode).                           ← logged once the watcher is live

If you see the first line but never the second, the app hasn't finished booting — fix the stalled dependency (or run pnpm nestjs-inertia codegen for a one-shot generation in the meantime).

Disable auto-watch (CI or explicit control):

InertiaModule.forRoot({
  codegen: { enabled: false },
});

Optional nest-cli.json snippet — only useful if your server bundle imports generated files:

{
  "compilerOptions": {
    "assets": [".nestjs-inertia/**/*"],
    "watchAssets": true
  }
}

Protocol parity

Full Inertia protocol: X-Inertia headers, version mismatch (409 + X-Inertia-Location, GET only), partial reloads, deferred props, merge/deepMerge with matchOn, once, history encryption / clear, error bags, X-Inertia-Reset, X-Inertia-Partial-Except, X-Inertia-Reset-Once, dot-notation unpacking, undefined→null wire conversion.

Companion packages (planned)

  • @dudousxd/nestjs-inertia-vite — Vite dev/build helpers (Plan B)
  • @dudousxd/nestjs-inertia-testingexpectInertia(res) matchers (Plan B)
  • @dudousxd/nestjs-inertia-codegen — typed pages (Plan C)
  • @dudousxd/nestjs-inertia-client — Tuyau-style typed REST + TanStack Query (Plan D)
  • Examples + docs site + CI workflows (Plan E)

See docs/design.md for full design.

License

MIT © Davi Carvalho