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

Route providers#

A route can provide services built from its own URL — the :userId in the path, its data, its query params, the value its guard resolved — with full type-safe dependency tracking.

Use it when a subtree's services depend on which route rendered them: a "current project" service, a tenant-scoped API client. Not when the dependency is global — provide it at the app level instead.

Build route-level providers from a route's own auto-provisioned tokens — path params, data, queryParams, and canActivate guarded data — with full, type-safe dependency tracking.

The problem#

A craftRoutes route auto-provisions route-scoped services. For a route query/:userId in the demo collection, craftRoutes generates helpers such as DemoUserIdParams and the yieldable DemoQueryUserIdGuardedData.

The params helper is useful inside a component and is consumed with yield*, exactly like a Craft service. Guarded data is consumed from a generator with yield* DemoQueryUserIdGuardedData(). Route data is intentionally not exported as a collection-level inject…Data helper; inside withProviders, consume it through the local Data generator. This also lets you take the value resolved by canActivate and feed it into a provider that the routed component injects.

The solution: craftRoute(...).withProviders(...)#

craftRoute(path, definition) authors a single route and returns a builder with a .withProviders(...) method. The callback receives route-scoped service generators, one per auto-provisioned token that exists on the route, and returns a normal providers array.

ts
import {
  abstract,
  craftRoutes,
  craftService,
  query,
  craftRoute,
} from '@craft-ts/core';

type User = { name: string };

// 1. An abstract contract — implemented per route.
const { UserRequirement, provideUser } = craftService(
  { name: 'User', scope: 'abstract' },
  abstract<User>(),
);

// 2. A guard that resolves the user.
const { Auth } = craftService({ name: 'Auth', providedIn: 'global' }, function* () {
  yield* query('auth', {
    params: () => true,
    loader: async () => ({}) as User,
  });
});

export const { demoRoutes } = craftRoutes('demo', [
  craftRoute('query/:userId', {
    componentDeps: {} as import('./query').GenDeps_GlobalQuery,
    loadComponent: ({ withRetry }) => withRetry(import('./query')),
    canActivate: function* () {
      const auth = yield* Auth.auth();
      const userValue = auth.value();
      if (!userValue) {
        return false;
      }
      return safeUser; // becomes the route's guarded data
    },
  }).withProviders(({ GuardedData }) => [
    provideUser(function* () {
      const guarded = yield* GuardedData(); // Signal<User>
      return guarded();
    }),
  ]),
]);

The routed component can now yield User() and receive the value that the guard resolved — without ever touching the fully-qualified route helper.

The helpers object#

The .withProviders(...) callback receives an object with route-local short names for every auto-provisioned token present on the route:

HelperPresent when…Yields
GuardedDatathe route has canActivateSignal<GuardData>
<Param>Paramsper path paramSignal<string> (e.g. UserIdParams)
QueryParamsthe route has queryParamsthe query-params state
Datathe route has dataSignal<RouteData>

Names are scoped to the single route, so the collection prefix and route path are dropped: GuardedData, not DemoQueryUserIdGuardedData. The path-param name is kept to keep multiple params distinct (UserIdParams, TeamIdParams, …).

Each helper is a generator you consume with yield*, exactly like a service's X():

ts
.withProviders(({ UserIdParams, QueryParams }) => [
  provideSomething(function* () {
    const userId = yield* UserIdParams();   // Signal<string>
    const qp = yield* QueryParams();        // query-params state
    return { userId, qp };
  }),
])

At collection level, a path parameter uses the same service-shaped name. For example, a craftRoutes('demo', [{ path: 'users/:userId', ... }]) collection exposes DemoUserIdParams:

ts
import { DemoUserIdParams } from './demo.routes';

const userId = yield* DemoUserIdParams(); // Signal<string>

Path parameters are exposed only through the service-shaped DemoUserIdParams() helper, so URL parameters participate in Craft's normal yieldable DI graph.

Pairing with an abstract service#

craftRoute(...).withProviders(...) shines with scope: 'abstract' services. The abstract service declares a contract; each route provides a concrete implementation derived from that route's data.

Abstract services now expose a provideX(factory) helper that takes a generator factory, tracks everything it yields, and binds the result to the requirement token. See Abstract services.

ts
const { User, provideUser } = craftService(
  { name: 'User', scope: 'abstract' },
  abstract<User>(),
);

// In a route:
.withProviders(({ GuardedData }) => [
  provideUser(function* () {
    return (yield* GuardedData())();
  }),
])

// In the routed component:
const user = yield* User(); // User

Dependency tracking & route DI#

Everything yielded inside a withProviders factory is tracked at the type level and folded into the route's dependency graph used by RouteCheckedDI:

  • The route's auto-provisioned tokens (guarded data, params, query params, data) are recognized as provided by the route itself — yielding them is always valid.

  • Any other service yielded inside the factory that is not provided by the route or the app surfaces as a missing-provider error, e.g.:

    The SomeService service is not provided in path: "query/:userId"
    
  • The provider's own name (User above) is registered as self-provided, so a component on that route can depend on it without a separate provider declaration.

This means the pattern is safe by construction: you cannot wire a route provider against data the route does not actually expose.

Plain providers still work#

.withProviders(...) is additive. A route can still declare a plain providers array, and both are merged (auto-provisioned services first, then providers, then the withProviders factory output):

ts
craftRoute('admin', {
  componentDeps: {} as import('./admin').GenDeps_Admin,
  loadComponent: ({ withRetry }) => withRetry(import('./admin')),
  providers: [SomeCraftProvider], // plain array, untyped helpers
}).withProviders(({ Data }) => [
  /* factory-built providers with tracking */
]);

Under the hood the builder stores the factory on a dedicated providersFn field, kept separate from the route's providers array.

See Also#

  • Setup — per-route DI checks

  • craftService — abstract scope, provideX, requirements

←ESLint rulesRoute guards→

On this page

  • The problem
  • The solution: craftRoute(...).withProviders(...)
  • The helpers object
  • Pairing with an abstract service
  • Dependency tracking & route DI
  • Plain providers still work
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search