How do I register a route?
// src/app/routes.ts
import { HomeView } from './views/home/home.view.js';
import { AdminView } from './views/admin/admin.view.js';
import { NotFoundView } from './views/not-found/not-found.view.js';
import { Guards } from './guards.js';
export const appRoutes: Nutin.Routes = {
'/': () => new HomeView(), // plain-function form
'/admin': { view: () => new AdminView(), guards: [Guards.requireAuth()] }, // guarded form
'/404': () => new NotFoundView(), // required — see below
};
// main.ts
import { AppRouter } from '../core/index.js';
import { appRoutes } from './routes.js';
AppRouter(appRoutes);
// In the global Nutin namespace: Nutin.RouteGuard, Nutin.ViewFactory, Nutin.RouteConfig, Nutin.Routes
type RouteGuard = (params: Record<string, string>) => boolean | string | Promise<boolean | string>;
type ViewFactory = () => View | Promise<View>;
type RouteConfig = ViewFactory | { view: ViewFactory; guards?: RouteGuard[] };
type Routes = Record<string, RouteConfig>;
- Route keys are path patterns — see How do I access route parameters? for the
:id/:id?syntax. - A plain-function entry (
() => new View()) has no guards. Use the{ view, guards }form when you need route guards — see How do I use route guards?. AppRouter(routes)installs the singletonRouterand immediately navigates to the current path — call it exactly once, at app startup. Calling it again elsewhere throws, sinceRouteris aServicesingleton.
Lazy routes
A view factory may return a Promise. Import the view's file with import() and the route becomes lazy: the view, and the code only it uses, go into their own chunk (dist/src/chunks/), downloaded the first time the route is visited instead of with the rest of the app.
// src/app/routes.ts
import { HomeView } from './views/home/home.view.js';
import { NotFoundView } from './views/not-found/not-found.view.js';
export const appRoutes: Nutin.Routes = {
'/': () => new HomeView(),
'/reports': () => import('./views/reports/reports.view.js').then((m) => new m.ReportsView()),
'/admin': {
view: () => import('./views/admin/admin.view.js').then((m) => new m.AdminView()),
guards: [Guards.requireAuth()],
},
'/404': () => new NotFoundView(),
};
- Import the view by its own file, never through a barrel (
views/index.ts,components/index.ts). A static import anywhere in the app, even of a barrel re-exporting it, puts a file back in the main bundle. The same goes for the components a lazy view uses: import them by their file in the view, so they follow it into its chunk. - Keep the first page eager. A lazy home page costs an extra request before anything renders. You should also keep
/404eager. - Navigation: guards run first, then the chunk loads. The current view stays on screen until the new one is ready. If another navigation starts meanwhile, the slower one is dropped.
- Failed loads: if the chunk can't be loaded (typically a deploy replaced the chunks while a tab was open, or the network dropped), the error is logged and the browser does a full page load of the target URL, which fetches the current build. If the same URL fails again within 10 seconds, it's only logged, so a broken chunk can't reload forever.
- SEO pages: with
generateSEOFiles, lazy routes are prerendered like the others.
Important: /404 is required
A '/404' entry must exist in your route table. If no route matches the current path and /404 isn't defined, the router logs console.error('No 404 route defined') and renders nothing — always include a not-found route.