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. Structuring the app
  2. Abstract services

Abstract services#

An abstract service declares a contract with no implementation, and forces a concrete one to be supplied downstream. This is what makes a service's implementation a decision of the mounting site — a route, a feature config, a test — instead of a hard import.

Abstract Requirements#

Use providedIn: 'abstract' to declare a contract that must be implemented elsewhere.

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

type CounterContract = {
  (): number;
  increment(): void;
};

const { CounterRequirement } = craftService(
  { name: 'Counter', providedIn: 'abstract' },
  abstract<CounterContract>(),
);

Concrete services can then depend on CounterRequirement.

Abstract Providers#

An abstract service also exposes a provideX(factory) helper. It takes a factory — a plain function or a generator — produces a value matching the contract, and binds it to the requirement token. This lets you implement the contract inline at the providing site (a route, a component, a feature config) instead of declaring a separate concrete craftService.

typescript
import { abstract, craftService } from '@craft-ts/core';

type User = { name: string };

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

// Implement the contract inline:
const providers = [provideUser(() => ({ name: 'Ada' }))];

// Anywhere downstream, inside a craft generator:
const user = yield * User();

The factory can be a generator that yields other services. Everything it yields is tracked, so the resulting provider participates in the cascade DI check just like a regular service:

typescript
const { Greeting } = craftService(
  { name: 'Greeting', providedIn: 'global' },
  function* () {
    yield* craftExpose('prefix', 'Hello');
  },
);

const providers = [
  provideUser(function* () {
    const greeting = yield* Greeting();
    return { name: `${greeting.prefix} Ada` };
  }),
];

An implementation returns its contract

Unlike a concrete craftService, which exposes the named primitives it yields, a provideX(factory) factory returns the value of its contract: the contract is a type (abstract<User>(), abstract<string>()), not a record of primitives.

This is the foundation of route-scoped providers: a route can implement an abstract contract from its own guarded data / params. See Type-safe DI/Routes → Route Providers.

See Also#

  • Service scopes

  • Route providers

←Shaping the public APIApp start→

On this page

  • Abstract Requirements
  • Abstract Providers
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search