Skip to content

Why Flare

What host.build() refuses and what it compiles.

Reviewed Updated 10 days ago · Flare 0.3

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.

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.

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.

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.