Typed Link
The @dudousxd/nestjs-inertia-client package ships typed <Link> components for React, Vue 3, and Svelte. Each component wraps the corresponding @inertiajs/* Link but adds a route prop that is autocompleted from your codegen-emitted RouteParamsMap, and a conditionally required routeParams prop typed to the exact parameters of that route.
Install
Section titled “Install”pnpm add @dudousxd/nestjs-inertia-clientModule augmentation
Section titled “Module augmentation”Running nestjs-inertia init scaffolds an augmentation stub in your frontend entry. If you haven’t run it yet, add the following to your frontend types file (e.g. src/inertia.d.ts):
import type { RouteParamsMap } from './.nestjs-inertia/routes.js';
declare module '@dudousxd/nestjs-inertia' { interface InertiaRegistry { routes: RouteParamsMap; }}Re-run codegen after adding new routes to keep RouteParamsMap up to date:
pnpm nestjs-inertia codegenBoot wiring
Section titled “Boot wiring”Each framework uses its idiomatic context/provider pattern to make the route() function available to all <Link> components in the tree.
Wrap your root component (or App) with <InertiaRouteProvider> in your Inertia entry file:
import { InertiaRouteProvider } from '@dudousxd/nestjs-inertia-client/react';import { route } from './.nestjs-inertia/routes.js';import { createInertiaApp } from '@inertiajs/react';import { createRoot } from 'react-dom/client';
createInertiaApp({ setup({ el, App, props }) { createRoot(el).render( <InertiaRouteProvider routes={route}> <App {...props} /> </InertiaRouteProvider>, ); },});Use app.provide() at the app level with INERTIA_ROUTES_KEY:
import { INERTIA_ROUTES_KEY } from '@dudousxd/nestjs-inertia-client/vue';import { route } from './.nestjs-inertia/routes.js';import { createInertiaApp } from '@inertiajs/vue3';import { createApp } from 'vue';
createInertiaApp({ setup({ el, App, props }) { const app = createApp(App, props); app.provide(INERTIA_ROUTES_KEY, route); app.mount(el); },});Svelte
Section titled “Svelte”Pass the resolver as Svelte context via mount()’s context option:
import { route } from './.nestjs-inertia/routes.js';import { createInertiaApp } from '@inertiajs/svelte';import { mount } from 'svelte';
const appContext = new Map([['inertia-routes', route]]);
createInertiaApp({ setup({ el, App, props }) { mount(App, { target: el, props, context: appContext }); },});Note: Svelte’s
setContext/getContextmust be called during component initialization. Thecontextoption passed tomount()is propagated to all child components, including<Link>components rendered by Inertia pages.
React example
Section titled “React example”import { Link } from '@dudousxd/nestjs-inertia-client/react';
// Route with no params — routeParams is omitted<Link route="users.index">All users</Link>
// Route with params — routeParams is required and typed<Link route="users.show" routeParams={{ id: '42' }}>View user</Link>Import from the /react subpath. The component accepts all standard @inertiajs/react Link props plus route and routeParams.
Vue example
Section titled “Vue example”<script setup lang="ts">import { Link } from '@dudousxd/nestjs-inertia-client/vue';</script>
<template> <!-- Route with no params --> <Link route="users.index">All users</Link>
<!-- Route with params --> <Link route="users.show" :routeParams="{ id: '42' }">View user</Link></template>Import from the /vue subpath. The component forwards all @inertiajs/vue3 Link props.
Svelte example
Section titled “Svelte example”<script lang="ts"> import Link from '@dudousxd/nestjs-inertia-client/svelte';</script>
<!-- Route with no params --><Link route="users.index">All users</Link>
<!-- Route with params --><Link route="users.show" routeParams={{ id: '42' }}>View user</Link>Import from the /svelte subpath. The component wraps @inertiajs/svelte’s Link.
How the route name is chosen
Section titled “How the route name is chosen”The route 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('users') | absent | users.<methodName> |
| absent | @As('profile') | <classNameStripped>.profile |
@As('users') | @As('profile') | users.profile |
@As('users.admin') | @As('profile') | users.admin.profile |
// Contract definition (server-side) — no name field neededimport { defineContract } from '@dudousxd/nestjs-inertia-client';
const ShowUser = defineContract({ params: z.object({ id: z.string() }), response: z.object({ id: z.string(), name: z.string() }),});
// Name is derived from UsersController.show → 'users.show'@Get('/users/:id')@ApplyContract(ShowUser)show() { ... }// Frontend usage — name comes from auto-derivation<Link route="users.show" routeParams={{ id: user.id }}>View profile</Link>Use @As at the class or method level (or both) to override:
import { As } from '@dudousxd/nestjs-inertia-client';
// Class-level @As sets the class portion for all methods in the class@Controller('/users')@As('users')class UsersController { @Get('/:id') @ApplyContract(ShowUser) @As('profile') // → 'users.profile' show() { ... }}Best practice: use the auto-derived name convention (UsersController.show → users.show) so that route('users.show') and api.users.show both point to the same endpoint — one typed URL builder, one typed data fetcher.
Conditional routeParams behaviour
Section titled “Conditional routeParams behaviour”routeParams is typed as a mapped type derived from RouteParamsMap:
- When the selected route has no dynamic segments (e.g.
/users), therouteParamsprop isneverand should be omitted. TypeScript will error if you pass it. - When the selected route has dynamic segments (e.g.
/users/:id), therouteParamsprop is{ id: string }and is required. TypeScript will error if you omit it.
This means all routing mistakes are caught at compile time — no runtime “missing param” errors.