Routing setup
Six steps turn Angular's routes into routes the compiler checks: a missing provider, a misspelled input or a route pointing at nothing becomes a build error instead of a blank screen.
Do this once per app, then let the CLI write new routes for you.
This guide assumes you are integrating type-safe DI/routes into an Angular app that consumes @craft-ng/core.
Prefer the guided version
Learn step 9 walks through the same setup on a single route, with the reasoning attached.
Prerequisites
Install the runtime package and the dev tooling in your app:
npm install @craft-ng/core
npm install -D @craft-ng/dev-tools1. Add a cascade DI check to every routes file
DI is checked next to the routes it covers. Every file containing craftRoutes(...) must pair its collection with ValidateCascadeRoutesFile and CanRun; a parent check deliberately does not descend through loadChildren.
import {
craftRoutes,
type CanRun,
type ValidateCascadeRoutesFile,
} from '@craft-ng/core';
import type { Router } from '@angular/router';
export const { appRoutes } = craftRoutes('app', [
/* routes */
]);
type _CheckAppDI = ValidateCascadeRoutesFile<never, Router, typeof appRoutes>;
type _CanRunApp = CanRun<_CheckAppDI>;ValidateCascadeRoutesFile compares:
- the generated dependencies declared on every route
- the providers available from the app, parent mount, route and component
If a route depends on a service that is not provided, or if a routed component expects an input that the route does not supply, _CanRunApp turns that mismatch into a TypeScript error in the routes file.
Typical errors look like:
The Counter service is not provided in path: "some-path"Input "userId" is not provided in path: "some-path"
2. Define routes with craftRoute and collect them with craftRoutes
Do not export a plain Angular Routes array directly. Define each typed route with craftRoute(...), collect them with craftRoutes(...), and declare componentDeps on each route component.
Breaking rename
The former route(...) helper has been renamed to craftRoute(...). There is no compatibility alias: update both the import and every call site.
import { craftRoute, craftRoutes } from '@craft-ng/core';
export const { appRoutes } = craftRoutes('app', [
craftRoute('', {
loadComponent: ({ withRetry }) => withRetry(import('./test')),
componentDeps: {} as import('./test').GenDeps_TestComponent,
}),
]);The important part is:
componentDeps: {} as import('./test').GenDeps_TestComponent,That line connects the generated GenDeps_* type of the component to the same-file cascade check.
Prefer the route CLI for day-to-day authoring
The CLI is the primary writing façade while the generated result remains ordinary editable TypeScript:
npx craft route add
npx craft route add /users/:userId --component src/app/users/user-detail.ts#UserDetailComponent
npx craft route add /users/:userId --create-component users/user-detailBy default it detects the Angular project and craftRoutes collections, creates one lazy routes file per feature, adds componentDeps, withRetry, .withParent, the parent mount assertion and the same-file DI check, then runs ESLint and TypeScript diagnostics. Use --dry-run to inspect the plan, --yes for non-interactive scripts and --json for machine-readable output.
Static redirects stay in the selected collection:
npx craft route add /old-users --redirect-to /users --parent src/app/app.routes.ts#appRoutesExisting flat groups can be split explicitly:
npx craft route split \
--parent src/app/app.routes.ts#appRoutes \
--prefix users \
--target src/app/users/users.routes.tsThe split command only moves statically analyzable routes. It reports local declarations or dynamic paths without mutating files, so business logic is never guessed.
Then wire the crafted routes into your application config:
import { craftAppConfig } from '@craft-ng/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { appRoutes } from './app.routes';
export const appConfig = craftAppConfig({
routingDeps: appRoutes.META_DATA,
providers: [provideRouter(appRoutes.toRoutes(), withComponentInputBinding())],
});Notes:
appRoutes.toRoutes()gives Angular the real runtime routes.appRoutes.META_DATAgivescraftAppConfig(...)the compile-time route dependency graph.- For non-blocking navigation (immediate URL commit, pending UI, centralised exception handling), render
CraftRouterOutlet()from@craft-ng/componentinside a Craft component tree instead of<router-outlet>, and useprovideCraftRouter(...)instead ofprovideRouter(...)— it accepts Angular router features and craft loading features (withErrorComponent,withRouteLoadError,withTransitionTimings, …) in one call, e.g.provideCraftRouter(appRoutes.toRoutes(), withComponentInputBinding(), withErrorComponent({ component: MyGlobalErrorScreen, componentDeps })). (The features also work standalone viaprovideCraftLoading(...).)withRouteLoadError(...)must stay inprovideCraftRouter(...)because it also registers an Angular navigation error handler and an internal recovery route. See Non-blocking navigation & pending UI and Route Load Errors. - For lazy routes,
loadChildrenshould return the named route tree exported by the child collection, for examplechildRoutes.childRoutes.
When a routes file gets big
The cascade check has a per-file budget, and past it TypeScript reports TS2589 and silently degrades inference in the whole file. The fix is to split into lazy child collections, each with its own check — see Scaling routes.
3. Run the Angular brand codemod through the published script
Add a script in your app:
{
"scripts": {
"craft:brand": "craft-brand --root src/app"
}
}Then run:
npm run craft:brandThis is the step that creates the initial GenDeps_* aliases in your component files, for example:
export type GenDeps_TestComponent = GetDeps<{
deps: {
CommonModule: CommonModule;
Counter: GetServiceDependencies<typeof Counter>;
};
provided: {};
publicProperties: GetPublicComponentProperties<TestComponent>;
}>;Adjust --root to your real source root:
src/appfor a standard Angular appprojects/my-app/src/appfor a workspace applibs/my-feature/srcfor a library
If you use a project-level craft-brand.config.ts, you can extend the script:
{
"scripts": {
"craft:brand": "craft-brand --root src/app --config ./craft-brand.config.ts"
}
}4. Install the ESLint rules
Several checks in this guide rely on code a rule generates or keeps in sync — GenDeps_* aliases, the same-file DI proof, the exhaustiveness assert. Others enforce the architecture itself.
Installing the plugin and the rule list is its own page: ESLint rules.
5. When a component changes, regenerate GenDeps with the Quick Fix
After changing a component's DI-related shape, refresh its generated alias.
Typical triggers:
- adding or removing
inject(...) - changing constructor injection
- changing component
imports - changing
providers - changing
viewProviders
Recommended workflow:
- first generation or bulk refactor:
npm run craft:brand - one file without
GenDeps_*: trigger the VS Code ESLint Quick Fix oncraft-ng/brand-angular-gen-deps-required - one file with
GenDeps_*: trigger the VS Code ESLint Quick Fix oncraft-ng/brand-angular-deps-match - CLI alternative for one file:
eslint --fix src/app/feature/my-component.ts
Important limits:
- the Quick Fix only handles the current file
- if you rename the component class, rerun the generator so the
GenDeps_*alias name stays aligned
WARNING
An Eslint error does not trigger a compilation error, so make sure to run the Quick Fix or eslint --fix after changing a component's DI shape. Otherwise, main.ts will not see the updated GenDeps_* and may miss real DI errors.
6. Validate the complete routing setup
Once the route files, generated GenDeps_* aliases and ESLint rules are in place, run the dedicated compile-time verification suite:
{
"scripts": {
"craft:verify-routes": "craft route verify --project tsconfig.app.json"
}
}npm run craft:verify-routesThe command first checks the existing route files: a component route must have an active CanRun/RouteCheckedDI proof (or a collection-level ValidateCascadeRoutesFile proof), and the exception, pending-component and lazy-retry ESLint bookkeeping must be complete. It then checks valid and intentionally invalid temporary fixtures for DI, route inputs, Angular/Craft template dependencies, pending and exception components, lazy retries, exception recovery and handler exhaustiveness. It refuses to run when the application already has type errors, and removes the fixtures afterwards. Use --json in CI tooling or --keep-fixtures to inspect a failed diagnostic.
See CLI automation for the full workflow and flags.
See Also
- CLI automation — let the CLI write routes for you
- Route guards — the next thing you'll add
- Scaling routes — when one routes file gets too big