Why Flare
What host.build() refuses and what it compiles.
A Flare app comes together in two phases. First you register: services with static deps, routes, contracts, config sections, all declared on a FlareHost. Then you call host.build(), which checks that whole graph and compiles it, or refuses to hand back an app. What that call refuses, and what it compiles, is most of what makes Flare different.
build()
Section titled “build()”Here’s an app with a wiring mistake: UserRepo is registered and declares a dependency on Db, but Db is never registered.
import { FlareHost, FlareResponse, FlareService } from "@flare-ts/core";import { node } from "@flare-ts/core/node";
class Db extends FlareService { public static override deps = [];
// db implementation...}
class UserRepo extends FlareService { public static override deps = [Db];
find(id: string) { // db.getById(...), etc.. return { id }; }}
const host = new FlareHost(node);host.scoped(UserRepo);
host.http.get("/users/:id", { inject: { repo: UserRepo } }, (ctx, { repo }) => { return new FlareResponse(200, repo.find(ctx.req.rawRouteParams.id ?? ""));});
host.build();[flare] Build failed with 1 validation error:
1. [UNDECLARED_DEPENDENCY] Service UserRepo has an undeclared dependency: Db. Hint: Register Db with host.scoped() or host.singleton() before calling host.build().The server never starts: build() throws, and nothing ever listens for a request. Because every relationship in the graph is declared, every relationship can be checked before traffic: a missing dependency, a state token no middleware provides, a contract entry that names no handler, a route registered twice. What would have been a 500 on some request weeks from now is a failed deploy with a name in it.
What build() compiles
Section titled “What build() compiles”Validation is half of build(); the other half is moving work off the request path. The rule across the framework: anything that can be answered before traffic is answered at build(), once.
Most of that is code generation. build() writes a function per route, shaped to that route’s pipeline: its before hooks, its handler, its after hooks, emitted in order as straight-line code. There is no step list to walk at request time and no per-request objects to reconcile, because the generated function holds exactly what that route has, in the order it has it. Even how a step is called, plain or chained, is settled while generating it, from how the hook was declared. Routes whose pipelines share a shape reuse one generated function.
The same idea runs through the rest of the framework: the router is generated from your route table, config is parsed and validated once into plain values, declared response schemas become compiled serializers, and an app with no per-request services shares one container.
What’s left at request time is matching a path, running that route’s function, and running your code.
One graph, both runtimes
Section titled “One graph, both runtimes”The graph doesn’t know what runtime it’s for. Hand FlareHost the node adapter and the built app has .run(); hand it cf and it has .export() for a Worker. Same services, same routes, same contracts. The adapter also types what the runtime can hold: host.singleton doesn’t exist on a Cloudflare host, so code that assumes process-lifetime state doesn’t typecheck.
Where next
Section titled “Where next”- What
build()does: the checks and the compile step, in order. - The arc model: how transport surfaces attach to the host.
- Dependency injection:
static deps, scoped vs singleton, the named inject map. - Tutorial: Your first app: a runnable path from zero.