Using DI in apps
static deps, ServiceToken and StateToken, inject guardrails, and build-time validator codes.
This page is the DI surface for application code: how a class declares dependencies, the token types, validator codes, and runtime guardrails. For why DI is explicit, see Overview. For lifetimes and Workers constraints, see Services and lifetimes.
Declaring dependencies
Section titled “Declaring dependencies”import { FlareService } from "@flare-ts/core";
class TagService extends FlareService { public static override deps = []; tag() { return "hello"; }}
class GreetService extends FlareService { public static override deps = [TagService];
readonly #tags = this.inject(TagService); greet() { return `tag: ${this.#tags.tag()}`; }}Three rules:
- Every service declares
static deps, even if it is[]. Omitting it throws when you callhost.scoped()orhost.singleton(), or when you register a controller, middleware, or error-handler class. It does not fail lazily atbuild(). this.inject(Token)only resolves tokens listed indeps. An unlisted token throws at the call site. A listed but unregistered token is caught athost.build()asUNDECLARED_DEPENDENCY(services) orCONTROLLER_UNREGISTERED_DEP/MIDDLEWARE_UNREGISTERED_DEP(controllers and middleware).- Resolution is lazy per request. Scoped services construct on first
inject()in a request, then cache for the rest of that request.
Controllers, middleware, and error handlers use the same static deps pattern with this.inject(Token).
Token types
Section titled “Token types”| Token | What it identifies | Registered with | Declared on class as |
|---|---|---|---|
ServiceToken | A DI-resolvable service | host.scoped / host.singleton | static deps |
StateToken | A per-request state slot | flareState() | static state / static provides |
ConfigToken | A flare.json section | host.cfg | static config |
State and config are validated at build() alongside services. See Request state and Configuration.
Register services on the host
Section titled “Register services on the host”import { FlareHost, FlareService } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
class DbConnection extends FlareService { public static override deps = [];}class MailService extends FlareService { public static override deps = [];}
const host = new FlareHost(node);host.scoped(DbConnection);host.singleton(MailService);List services in controller static deps before calling this.inject:
import { ControllerBase, FlareHost, FlareService } from "@flare-ts/core";import { Get } from "@flare-ts/core/decorators";import { node } from "@flare-ts/core/node";
class DbConnection extends FlareService { public static override deps = []; ping() { return "ok"; }}
class HealthController extends ControllerBase { public static override deps = [DbConnection]; public static override state = [];
@Get("/health") check() { return this.ok({ db: this.inject(DbConnection).ping() }); }}
const host = new FlareHost(node);host.scoped(DbConnection);host.http.controller("", HealthController);
const app = host.build();app.run();Build-time validator codes
Section titled “Build-time validator codes”| Code | When | Fix |
|---|---|---|
UNDECLARED_DEPENDENCY | Service deps entry not registered | host.scoped() / host.singleton() before build() |
CIRCULAR_DEPENDENCY | Cycle in service graph | Refactor or introduce an intermediary |
CAPTIVE_DEPENDENCY | Singleton deps includes a scoped token | Pass scoped data via method args in handlers |
CONTROLLER_UNREGISTERED_DEP | Controller or inline route inject references unregistered service | Register the service on the host |
MIDDLEWARE_UNREGISTERED_DEP | Middleware deps references unregistered service | Register the service on the host |
Full catalog: Failure modes.
In test mode, app.test({ replace }) re-runs the service validator after substitutions and throws FlareTestError when the replacement graph is invalid.
Runtime guardrails
Section titled “Runtime guardrails”These throw plain Error during a request:
| Trigger | Result |
|---|---|
inject() token not in static deps | Message names class and token; tells you to add to deps |
| Scope missing a registered token | ServiceToken … not registered in container. |
| Circular resolution inside factories | Circular dependency detected while resolving |
Missing static deps at registration | TagService is missing static 'deps'. |
Replacing services in tests
Section titled “Replacing services in tests”import { FlareHost, FlareService } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
class Db extends FlareService { public static override deps = []; query() { return { ok: true, url: "real" }; }}
class StubDb extends Db { override query() { return { ok: true, url: "stub" }; }}
const host = new FlareHost(node);host.scoped(Db);
const app = host.build();const handle = await app.test({ replace: new Map([[Db, StubDb]]) });
await handle.reset({ replace: new Map([[Db, StubDb]]) });await handle.reset();Rules: replacement must extend the token; key must be registered; validator re-runs after substitution. See Replace services in tests.
For unit tests without a full host, use mockContainer from @flare-ts/core/testing.
Related
Section titled “Related”- Named inject map: inline route handlers
- Services and lifetimes: scoped vs singleton
- FlareService: base class API