Skip to content

Overview

Why Flare resolves only the dependencies you declare in static deps, fails a broken graph at build(), and reads no decorator metadata.

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

Flare’s dependency injection is explicit and static. Every class declares what it depends on with a static deps array. The host resolves only what you listed, fails loudly at build() when the graph is broken, and never scans constructors or reads decorator metadata.

That is a deliberate choice. The rest of this page is the why. For registration, lifetimes, inline inject, and validator codes, see Using DI in apps and Services and lifetimes.

A class lists its tokens, and the framework resolves only those:

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()}`;
}
}

Nothing is inferred. deps is a plain array the host reads, not a set of constructor types it discovers. The payoff is that the whole dependency graph is known before any request runs: the host can check at build() that every token in every deps array was actually registered, and throw when one was not, rather than waiting for the request that first injects a missing service.

The cost is a line of boilerplate per class. The explicitness is what lets the graph be checked ahead of traffic.

The thing a declared graph replaces is often not a fancier container at all; it is the hand-wired module singleton: export const db = new Db(), imported wherever it is needed. That works until it does not. The instance is created the first time its module loads, so its lifetime is import order rather than a choice you made, every consumer shares the one instance, and swapping it in a test means monkeypatching the module. Declaring the graph hands each of those decisions to the host instead: the lifetime is the one you registered, a scoped token is a fresh instance per request, and a test substitution is a replace entry the validator re-checks.

At request time the HTTP arc hands each controller, middleware, and service a per-request scope. Resolution from that scope follows one order: a pre-built singleton is returned if one exists; otherwise a scoped service is built by its factory, run once, and cached for the rest of the request. Order follows the dependency graph, not registration order. When the handler pipeline finishes, scoped instances are disposed in reverse creation order.

A scoped service is not built until the first inject() that needs it, and once built it is the same instance for every later inject() in that request. Two requests never share a scoped instance. See Services and lifetimes for the full table.

Request state rides alongside this. A StateToken is a per-request slot that middleware writes and handlers read; the host checks at build() that something provides every state token a route declares. The same build step that validates the service graph also validates the state graph.

The usual TypeScript answer to DI is constructor scanning: decorate a constructor, let reflect-metadata record parameter types, and resolve them at runtime. Flare does not do that:

  • Failures before traffic. A static deps array is readable at build() without constructing anything. Constructor metadata surfaces missing wiring when a request hits the broken handler.
  • No toolchain requirements. Constructor scanning needs experimentalDecorators, emitDecoratorMetadata, and the reflect-metadata package. static deps is a plain array.
  • Consistent behavior across runtimes. A static array read at startup behaves the same on Node, Workers, Bun, and Deno.

HTTP method decorators (@Get, @Post, and so on from @flare-ts/core/decorators) record routing metadata only. The DI machinery never reads them.