Skip to content

Using DI in apps

static deps, ServiceToken and StateToken, inject guardrails, and build-time validator codes.

AI generated, pending review Updated 10 days ago · Flare 0.3

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.

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:

  1. Every service declares static deps, even if it is []. Omitting it throws when you call host.scoped() or host.singleton(), or when you register a controller, middleware, or error-handler class. It does not fail lazily at build().
  2. this.inject(Token) only resolves tokens listed in deps. An unlisted token throws at the call site. A listed but unregistered token is caught at host.build() as UNDECLARED_DEPENDENCY (services) or CONTROLLER_UNREGISTERED_DEP / MIDDLEWARE_UNREGISTERED_DEP (controllers and middleware).
  3. 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).

TokenWhat it identifiesRegistered withDeclared on class as
ServiceTokenA DI-resolvable servicehost.scoped / host.singletonstatic deps
StateTokenA per-request state slotflareState()static state / static provides
ConfigTokenA flare.json sectionhost.cfgstatic config

State and config are validated at build() alongside services. See Request state and Configuration.

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();
CodeWhenFix
UNDECLARED_DEPENDENCYService deps entry not registeredhost.scoped() / host.singleton() before build()
CIRCULAR_DEPENDENCYCycle in service graphRefactor or introduce an intermediary
CAPTIVE_DEPENDENCYSingleton deps includes a scoped tokenPass scoped data via method args in handlers
CONTROLLER_UNREGISTERED_DEPController or inline route inject references unregistered serviceRegister the service on the host
MIDDLEWARE_UNREGISTERED_DEPMiddleware deps references unregistered serviceRegister 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.

These throw plain Error during a request:

TriggerResult
inject() token not in static depsMessage names class and token; tells you to add to deps
Scope missing a registered tokenServiceToken … not registered in container.
Circular resolution inside factoriesCircular dependency detected while resolving
Missing static deps at registrationTagService is missing static 'deps'.
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.