Skip to content

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.

Terminal window
pnpm add @dudousxd/nestjs-inertia-client

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:

Terminal window
pnpm nestjs-inertia codegen

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);
},
});

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/getContext must be called during component initialization. The context option passed to mount() is propagated to all child components, including <Link> components rendered by Inertia pages.

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.

<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.

<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.

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 the Controller suffix stripped and first letter lowercased.
  • Method portion: method-level @As(...) value if present, otherwise the method name.
  • Final name: ${classPortion}.${methodPortion}
Class-level @AsMethod-level @AsDerived name
absentabsent<classNameStripped>.<methodName> (default)
@As('users')absentusers.<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 needed
import { 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.showusers.show) so that route('users.show') and api.users.show both point to the same endpoint — one typed URL builder, one typed data fetcher.

routeParams is typed as a mapped type derived from RouteParamsMap:

  • When the selected route has no dynamic segments (e.g. /users), the routeParams prop is never and should be omitted. TypeScript will error if you pass it.
  • When the selected route has dynamic segments (e.g. /users/:id), the routeParams prop 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.