Build-time validation
How a Flare app is assembled and what host.build() validates before the server accepts traffic.
A Flare app is assembled by registering a graph of tokens, classes, and routes on FlareHost, then calling host.build() to validate the entire graph and compile per-route pipelines before the server accepts traffic. Registration is explicit: call host.cfg(), host.scoped() or host.singleton(), and host.http.* (plus host.ws.*, and any adapter-stamped surfaces) before build(). Registrations after the first successful build() are ignored because later calls return the cached app.
The three pieces
Section titled “The three pieces”// @ground:preambleimport { FlareService, ControllerBase, flareConfig } from "@flare-ts/core";import { str } from "@flare-ts/lib/schema";
const DbConfig = flareConfig("db", { url: str });
class DbService extends FlareService { public static override deps = [];}
class ApiController extends ControllerBase { public static override deps = [];}import { FlareHost } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
const host = new FlareHost(node);
host.cfg(DbConfig); // 1. config tokenshost.singleton(DbService); // 2. service registrationshost.http.controller("/api", ApiController); // 3. routes / arcs- Config tokens: sections of
flare.jsonorFLARE__env vars you want typed (flareConfig("db", { ... })). Register each withhost.cfg(token)so the host parses and validates that section duringbuild(). - Service registrations: classes the DI container resolves. Use
host.scoped()for per-request instances. Usehost.singleton()for per-process instances on Node (singletons are created atbuild()in production). On Cloudflare Workers,host.singleton()throws at registration time; register the same class withhost.scoped()instead. Both methods require astatic depsarray (may be[]). Omittingstatic depsthrows when you callhost.scoped()orhost.singleton(), not atbuild(). - Routes / arcs: inline handlers, controllers, middleware, and groups on
host.httpandhost.ws. State tokens enter the graph when routes declarestate: [...]and middleware declaresprovides: [...].
Logger transports register on host.logging. Error handlers register on host.http.error(). CORS registers on host.http.cors(). See Host for the full registration table.
What build() validates
Section titled “What build() validates”host.build() is synchronous and runs in order: config compilation, logger bootstrap, three composite validator suites, then arc compilation (pipelines, router, state wiring).
compilation succeeds (such as DEAD_MIDDLEWARE and ORPHANED_CONTRACT_ENTRY).
After validators pass, the HTTP arc compiles pipelines. State provisioning is checked here: if a controller’s static state or a middleware class’s static state includes a token that no earlier middleware provides in a before() hook, compilation throws a plain Error, not a FlareValidationError. See The arc model for provisioning rules.
The three validator families
Section titled “The three validator families”The three suites map one-to-one onto the three things you registered, and they run in the order the graph is layered: services first, then HTTP, then config.
- The service graph is checked first because every other layer resolves out of it. This family answers “can the DI container build what every class asked for”: dependencies that point at a token nobody registered, cycles in the graph, a singleton holding a scoped instance hostage, a lifecycle hook on a class that has no such phase. The representative is
UNDECLARED_DEPENDENCY: a class listsFooinstatic depsbutFoowas never registered. If a mistake is about which service is wired to which, it fails here. - The HTTP wiring is checked next, on top of a service graph already known to resolve. This family answers “does the transport surface fit together”: malformed paths, two routes claiming the same method, a route or query param naming collision, a state-provider cycle across global middleware, an invalid CORS shape. The representative is
MIDDLEWARE_STATE_CYCLE: two pieces of global middleware each need a state token the other provides. If a mistake is about a path, a route, or how middleware hands state along, it fails here. - The config tokens are checked last because they sit at the edge of the graph, declared against by the classes the first two families already validated. This family answers “is the config the classes asked for actually present”: a class declaring
static configfor a tokenhost.cfg()never registered, or a registered token whoseflare.jsonsection or required field is missing. The representative isUNREGISTERED_CONFIG_TOKEN: a class declaresstatic config = [DbConfig]buthost.cfg(DbConfig)was never called. If a mistake is about a config section or field, it fails here.
The suites do not stop at the first error. Every validator in every family runs and the errors are collected, so one build() reports every problem across all three families at once, not just the first one found. The order above is the order they are reported in, and it lets you place an unfamiliar mistake before you read the code: a wiring problem about services lands in the first family, about routes or middleware in the second, about config in the third.
Two related checks sit just outside this set. Contract body and query schemas validate on each request, not at build (invalid bodies return 400); build time checks contract wiring only: it warns ORPHANED_CONTRACT_ENTRY for a handler-less contract key and errors with CONTRACT_KIND_MISMATCH when a controller carries a non-http contract (for example a socketContract). And app.test({ replace }) re-runs the service family against the post-replacement graph, throwing FlareTestError rather than FlareValidationError when a replacement does not extend the token it replaces.
See Failure modes for the full code catalog, example messages, and registration-time errors.
Failure before traffic
Section titled “Failure before traffic”Validator and compile failures both happen before the server binds a port:
$ node src/main.tsError: NoDepsService is missing static 'deps'. at FlareHost.scoped (...)$ node src/main.tsError: [flare] Build failed with 1 validation error:
1. [UNDECLARED_DEPENDENCY] Service GreetService has an undeclared dependency: TagService. Hint: Register TagService with host.scoped() or host.singleton() before calling host.build().$ node src/main.tsError: MeController requires state token AuthUser that is not provided by any preceding middleware. Please ensure that a preceding middleware in the chain provides this state token.A deploy that fails this way never accepts traffic with a half-built pipeline.
Idempotent build
Section titled “Idempotent build”host.build() is safe to call multiple times. The second call returns the cached app:
import { FlareHost } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
const host = new FlareHost(node);
const app = host.build(); // compilesconst same = host.build(); // returns the same app instanceThis matters in tests. A typical entry file calls build() and run() at module scope. When your test imports that module, build() has already run. Calling host.build() again returns the same compiled app; validators and pipeline compilation do not run twice. To drive requests, call app.test() on that cached app to get a TestAppHandle. See Testing.
In FLARE_MODE=test, scoped and singleton compilation are deferred until app.test({ replace }) so test doubles can substitute classes before any constructor runs. HTTP arc compilation still runs on the first host.build(). When app.test() runs, scoped and singleton registrations compile so substituted classes can instantiate (pipelines and router are not recompiled).
Related
Section titled “Related”- What build() does: mental model for the compile step
- Failure modes: full validator and error catalog
- The arc model: pipeline order and state provisioning