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. Recommended approaches
  2. Inject at the point of use

Inject at the point of use#

This page introduces the first recommended approach for structuring a Craft application. The useful rule is simple:

Get what you need where you need it.

Declare a dependency in the smallest factory that actually uses it. If a query needs an API method, the query yields that method. If a route guard needs the current user, the guard yields the user service. There is no need to add an intermediary method to a component just to forward the call.

The forwarding shape to avoid#

Even with Craft, do not resolve an API in the component only to forward it into a query:

typescript
export const Tasks = craftComponent('Tasks', {}, function* () {
  const api = yield* TaskApi();
  const tasks = yield* query('tasks', {
    params: () => true,
    loader: function* () {
      return yield* api.list();
    },
  });

  return /* … */;
});

The loader closes over api, so the query itself does not declare the operation it uses. The dependency is attached to the component instead of to the smallest factory that performs the request.

Craft puts the dependency next to the work#

With Craft, the component declares the query directly, and the query yields exactly the API operation it needs. In this example, TaskApi is a crafted service (or a small boundary adapter):

typescript
import {
  craftComponent,
  forNode,
  ifNode,
  li,
  p,
  ul,
} from '@craft-ts/component';
import { query } from '@craft-ts/core';

export const Tasks = craftComponent('Tasks', {}, function* () {
  const tasks = yield* query('tasks', {
    params: () => true,
    loader: function* () {
      return yield* TaskApi.list();
    },
  });

  return ifNode(
    tasks.isLoading,
    () => p('Loading…'),
    () =>
      ul(
        forNode(
          () => tasks.value() ?? [],
          { track: (task) => task.id },
          (task) => li(task.title),
        ),
      ),
  );
});

TaskApi.list() is yielded directly from the query loader. The query owns the server state, while the template owns only the rendering of that state. There is no loadTasks() method, no subscription, and no extra service whose only job is to forward this request.

Why this is useful#

The dependency graph is explicit#

yield* TaskApi.list() is part of the loader's dependency type. Craft can use the same information for route DI checks, test registers, and dependency snapshots. A missing provider or mock is found at the boundary where it matters.

Dependencies stay granular#

When a consumer needs one operation, yield that operation instead of the whole service:

typescript
const list = yield* TaskApi.list();

The graph records the property that was used. Tests only need to provide list, and future changes to unrelated API methods do not expand this consumer's contract.

Async behaviour has one owner#

query derives the loading, value, and exception state. The component does not need a second signal, subscription, or manual error flag that could drift away from the request.

The rule of thumb#

  • If a query or mutation needs an API operation, yield it in that query or mutation.

  • If a service needs another service, yield the dependency in that service's factory.

  • If a component needs a dependency directly, yield it in the component's factory.

  • Create a dedicated service when it owns reusable behaviour or a meaningful boundary — not merely to forward one method call.

Direct does not mean unstructured. The dependency is still named, tracked, scoped, mockable, and exposed through a deliberate public API. It simply lives close to the code that uses it.

See also#

  • The mental model — declare, yield, derive

  • craftService — define and compose services

  • Shaping a service's public API — expose only what a consumer needs

  • Testing services — test the same dependency graph

  • Architecture rules — constraints across that graph

←Schema validationcraftService→

On this page

  • The forwarding shape to avoid
  • Craft puts the dependency next to the work
  • Why this is useful
  • The dependency graph is explicit
  • Dependencies stay granular
  • Async behaviour has one owner
  • The rule of thumb
  • See also

@craft-ts · MIT licensed

GitHubnpm

Search