Aviary
Packages

@dudousxd/nestjs-inertia (core)

Core NestJS module, @Inertia decorator, InertiaService, and version negotiation.

pnpm add @dudousxd/nestjs-inertia
npm install @dudousxd/nestjs-inertia
yarn add @dudousxd/nestjs-inertia

Implements the Inertia.js protocol as a NestJS module. Handles page rendering, version negotiation, shared props, SSR, CSRF, and redirects.

Exports

ExportDescription
InertiaModule.forRoot(options)Global module registration; sets root view, version, shared props
InertiaModule.forRootAsync(options)Async variant (useFactory, useClass, useExisting)
InertiaModule.forFeature(options)Register an additional scope with its own view/shared props
InertiaModule.forFeatureAsync(options)Async variant for feature scopes
@Inertia(component)Method decorator; intercepts return value, renders Inertia response
@UseInertia(scope)Controller/method decorator; selects a forFeature scope
InertiaServiceInjectable service for programmatic renders and redirects
Inertia.always(fn)Prop marker; always resolved, even on partial reloads
Inertia.optional(fn)Prop marker; resolved only when explicitly requested
Inertia.defer(fn, group?)Prop marker; deferred to a separate request
Inertia.merge(fn, opts?)Prop marker; appended/prepended on reload
Inertia.once(fn, opts?)Prop marker; resolved once, then cached client-side
Inertia.scroll(fn, opts?)Prop marker; infinite-scroll pagination — inner data merged across visits with an announced cursor
InertiaValidationFilterException filter; flashes a field-keyed error bag and 303-redirects back on validation failure (Forms & Validation)
inertiaValidationExceptionFactoryValidationPipe exception factory that preserves nested field paths
ErrorBagInterceptorNamespaces errors by X-Inertia-Error-Bag header
CsrfCookieInterceptorSets XSRF-TOKEN cookie and validates X-XSRF-TOKEN
CsrfGuardGuard-based CSRF validation
generateCsrfToken / verifyCsrfToken / rotateCsrfTokenStandalone HMAC CSRF token functions (see CSRF)
RedirectInterceptorConverts 302 to 303 for PUT/PATCH/DELETE per Inertia spec
MethodSpoofMiddleware_method field support for HTML form method spoofing

Minimal setup

import { resolve } from 'node:path';

InertiaModule.forRoot({
  rootView: resolve(__dirname, '../inertia/root.html'),
  version: '1',
})

See the Getting Started guide for a full bootstrap example.

InertiaService runtime methods

Inject InertiaService to drive a response programmatically. Beyond render(), it exposes the full set of Inertia protocol operations. The mutators (share, encryptHistory, clearHistory) return this, so they chain.

MethodEffect
share(input)Merge extra props into the page's shared props for this response (object or a resolver function). Chainable.
flash(data) / flash(key, value)Flash non-error data for the next request (Laravel/Rails-style). Persisted via the configured flashStore and surfaced as the flash shared prop on the following render, then cleared. No-ops when no flashStore is set.
location(url)Perform an Inertia redirect: for Inertia XHR it sends 409 with X-Inertia-Location; for a plain browser visit it sends 302 Location. Only relative or same-origin absolute URLs are accepted (open-redirect safe).
encryptHistory(value = true)Ask the client to encrypt this page's history entry (protects props cached in history.state). Chainable.
clearHistory()Tell the client to clear its encrypted history cache — call it after logout/session changes. Chainable.
account.controller.ts
@Controller('account')
export class AccountController {
  constructor(private readonly inertia: InertiaService) {}

  @Post('logout')
  async logout() {
    await this.inertia.flash('status', 'signed-out');
    // wipe any encrypted history, then bounce to /login (409 for XHR, 302 for a full visit)
    this.inertia.clearHistory().location('/login');
  }

  @Get('secret')
  async secret() {
    // encrypt this entry's history and add a per-response shared prop
    this.inertia
      .encryptHistory()
      .share({ banner: 'Confidential' });
    await this.inertia.render('account/secret', { balance: 4200 });
  }
}

flash() and the validation error bag both ride on the configured flashStore. Wire one up (any store implementing writeFlash / write) to enable both — see Forms & Validation.

CSRF

CsrfGuard and CsrfCookieInterceptor are the drop-in pair (issue an XSRF-TOKEN cookie, validate the X-XSRF-TOKEN header). Under them sits a standalone, DI-free HMAC token API you can call directly — for custom middleware, a login handler, or tests:

ExportSignaturePurpose
generateCsrfToken(secret, context?) => stringMint a token. With a context (e.g. session id), it's bound into the HMAC as raw.ctx.sig, so the token dies when the context changes.
verifyCsrfToken(token, secret, context?) => booleanConstant-time verify. Returns false (never throws) on any malformed/mismatched token.
rotateCsrfToken(res, options, context?) => voidWrite a fresh token cookie onto the response — call after a session principal change to prevent fixation.

Both CsrfGuardOptions and CsrfCookieOptions accept a secret, optional cookieName (default XSRF-TOKEN) / headerName (default X-XSRF-TOKEN), and an optional tokenContext: (req) => string — supply the same tokenContext to the guard and the interceptor so issued and verified tokens agree.

auth.controller.ts
import { generateCsrfToken, rotateCsrfToken, verifyCsrfToken } from '@dudousxd/nestjs-inertia';

// bind the token to the session so old tokens stop verifying after re-login
const token = generateCsrfToken(secret, req.session.id);
verifyCsrfToken(token, secret, req.session.id); // true

// after issuing a new session, rotate the cookie to kill fixation
rotateCsrfToken(res, { secret, sameSite: 'lax' }, req.session.id);
app.module.ts — the guard/interceptor pair
const tokenContext = (req: { session?: { id?: string } }) => req.session?.id ?? '';

{
  provide: APP_GUARD,
  useValue: new CsrfGuard({ secret: process.env.CSRF_SECRET!, tokenContext }),
}
// paired with a CsrfCookieInterceptor that shares the same secret + tokenContext

On this page