Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Routing & type-safe DI
  2. Scaling routes

Scaling routes#

RouteCheckedDI validates one routed component at a time. Its cost does not grow with the number of sibling routes, so a large route file keeps the same DI safety as a small one.

Keep route ownership clear

Use loadChildren when a feature deserves its own lazy boundary or team ownership. Every route file remains independently checked.

Large route files#

The per-route check does not recursively instantiate the complete route tuple. Add one RouteCheckedDI / CanRun pair for each routed component:

ts
import { type CanRun, type RouteCheckedDI } from '@craft-ts/core';

type _CheckItem0 = RouteCheckedDI<
  import('./item-0').GenDeps_Item0Component,
  AppProvidedNames,
  AppProvidedValues,
  'Item0Component'
>;
type _CanRunItem0 = CanRun<_CheckItem0>;

type _CheckItem1 = RouteCheckedDI<
  import('./item-1').GenDeps_Item1Component,
  AppProvidedNames,
  AppProvidedValues,
  'Item1Component'
>;
type _CanRunItem1 = CanRun<_CheckItem1>;

If a component starts depending on a service that is not provided, or expects an input that the route does not supply, its CanRun alias becomes a TypeScript error in the route file.

Scaling to hundreds of routes#

Organise routes as a tree of feature files joined by loadChildren when that improves lazy loading or ownership:

app.routes.ts
├── billing.routes.ts
├── admin.routes.ts
└── reporting.routes.ts

The parent registers a child collection with a lazy entry:

ts
{
  path: 'billing',
  loadChildren: ({ withRetry }) =>
    withRetry(import('./billing.routes')).then((m) => m.billingRoutes),
},

The child file declares its own routes and its own per-route checks. A parent proof never covers a component inside a loadChildren collection.

Threading the parent DI context

The second and third RouteCheckedDI parameters are the names and values provided at the route's mount point — app providers plus ancestor route providers. When an ancestor adds providers, re-export that cumulative context and pass it to the child route checks.

Pinning a lazy child to its mount path (.withParent + assertChildRouteMounts)#

Splitting into loadChildren keeps route ownership clear, but nothing yet guarantees a child is wired under the right parent route. A child whose components rely on a specific mount — its :photoId param, a declared view-transition payload, or an ancestor's providers — is only correct under that path.

Pin a collection to its mount path with .withParent<ParentRoutes<'path'>>(), then enforce it once in the parent with assertChildRouteMounts(parentRoutes):

ts
// view-transitions.routes.ts — the child declares where it belongs
import { craftRoutes, craftRoute, type ParentRoutes } from '@craft-ts/core';

export const { viewTransitionsRoutes } = craftRoutes('viewTransitions', [
  craftRoute(':photoId', {
    componentDeps: {} as import('./photo-detail').GenDeps_PhotoDetailComponent,
    loadComponent: ({ withRetry }) => withRetry(import('./photo-detail')),
  }),
]).withParent<ParentRoutes<'view-transitions'>>();
ts
// app.routes.ts — the parent enforces placement
import { assertChildRouteMounts, craftRoutes } from '@craft-ts/core';

export const { demoRoutes } = craftRoutes('demo', [
  {
    path: 'view-transitions',
    loadChildren: ({ withRetry }) =>
      withRetry(import('./view-transitions.routes')).then(
        (m) => m.viewTransitionsRoutes,
      ),
  },
]);

assertChildRouteMounts(demoRoutes);

Mounting the pinned collection under another path fails in the parent file:

craftRoutes(...).withParent<ParentRoutes<'view-transitions'>>() must be
loadChildren-mounted under the route with path 'view-transitions', not 'admin'

Notes:

  • A collection without .withParent is unpinned and can be mounted anywhere.

  • assertChildRouteMounts reads only the parent's own routes; it does not re-validate the child.

  • .withParent<…>() is type-only and creates no runtime coupling.

  • craft-ts/require-child-route-mount-check adds the missing assertChildRouteMounts(...) call and import on --fix.

See Also#

  • Setup

  • Architecture rules — assertRouteDiProofs catches a routed component with no check

  • Route providers

←Route load errorsComponents→

On this page

  • Large route files
  • Scaling to hundreds of routes
  • Pinning a lazy child to its mount path (.withParent + assertChildRouteMounts)
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search