Skip to content
Flare 0.1.x is pre-release. Expect breaking changes before 1.0 — see the changelog.

Host

FlareHost is the composition root: one object that holds your config tokens, services, routes, and runtime adapter. Call host.build() to get a compiled app. Then call the entrypoint that matches your runtime: app.run() on Node, app.export() on Cloudflare Workers, or await app.test() when FLARE_MODE=test is set in the test runner.

import { FlareHost } from "@flare-ts/core";
import { node } from "@flare-ts/core/node";
const host = new FlareHost(node);

The constructor takes a runtime adapter:

AdapterImportUse for
nodeimport { node } from "@flare-ts/core/node"Node.js 22+; reads flare.json from the project root (missing file → defaults + env)
cfimport { cf } from "@flare-ts/core/cloudflare"Cloudflare Workers; reads FLARE__* overrides from adapter.env (process.env), e.g. via wrangler.toml [vars]
buildCf(flareJson)import { buildCf } from "@flare-ts/core/cloudflare"Cloudflare Workers; config from a bundled flare.json import
bunimport { bun } from "@flare-ts/core/bun"Stub only; build() throws when the adapter creates the app
denoimport { deno } from "@flare-ts/core/deno"Stub only; build() throws when the adapter creates the app

For Workers with FLARE__* values in wrangler.toml [vars]:

import { FlareHost } from "@flare-ts/core";
import { cf } from "@flare-ts/core/cloudflare";
const host = new FlareHost(cf);

For Workers with a bundled flare.json:

import flareJson from "./flare.json" with { type: "json" };
import { FlareHost } from "@flare-ts/core";
import { buildCf } from "@flare-ts/core/cloudflare";
const host = new FlareHost(buildCf(flareJson));
MethodRegistersNotes
host.cfg(...tokens)Config tokens from flareConfig()Required for any class declaring static config. Returns host for chaining. See Config.
host.scoped(ServiceClass)A per-request serviceClass must have static deps (may be []); throws if missing.
host.singleton(ServiceClass)A per-process serviceThrows on Cloudflare Workers at registration; use host.scoped() there instead. In production mode, instances are created at build(); in test mode, creation is deferred until app.test(). onStart() runs when the app starts (run(), export(), or test()).
host.http.get/post/put/patch/deleteInline HTTP routeSee Routes
host.http.controller(prefix, Cls)A ControllerBase at a prefixSee Routes
host.http.use(Cls)A global MiddlewareBaseSee Middleware
host.http.before/after/finallyInline global middleware hooksSee Middleware
host.http.group(prefix, fn)A route group with a shared prefixSee Routes
host.http.cors(config)Arc-level CORS policySee HTTP
host.http.error(fn)HTTP error mapperCatches thrown errors and maps them to responses. See Middleware
host.logging.transport(Cls)A logger transportSee Logger

Register everything in the host module before the first host.build() call. A second build() returns the cached app and does not recompile, so registrations added after the first build() never reach the running app. If you need more routes or services, add them above host.build() in the same module (or split registration into an imported setup function that runs before build()).

const app = host.build();

host.build() is synchronous. It resolves config, bootstraps the logger, runs the service/HTTP/config validator suites, and compiles the HTTP arc. In production mode it also compiles scoped and singleton registrations into the DI graph. Validator failures throw FlareValidationError before anything binds a port or exports a handler. Config parse failures throw a plain Error earlier in the same call. A second call returns the same cached app with no extra work.

The return type depends on the adapter and test mode:

Runtime / modehost.build() return valueEntrypoint method
Node (production)App with run()app.run()
Cloudflare (production)App with export()app.export()
Any adapter + FLARE_MODE=testTest app with test(), plus no-op run() / export()await app.test()
AccessorWhat it gives you
host.configResolved config snapshot. Empty {} before build().
host.loggerBootstrapped Logger after build(). Reading it earlier throws a plain Error; call host.build() before accessing host.logger.
host.stateCurrent lifecycle state string. See Lifecycle state.
host.scopedServicesRead-only view of the scoped service registry (get, tokens, length).
host.singletonServicesRead-only map of compiled singleton instances. In test mode before app.test(), only pre-built singletons (such as Logger) appear here.
import type { NodeRunHandle, NodeRunOptions } from "@flare-ts/core/node";
const app = host.build();
const handle: NodeRunHandle = app.run(
{
port: 3000,
host: "0.0.0.0",
shutdownTimeout: 10_000,
} satisfies NodeRunOptions,
);
// graceful shutdown:
await handle.stop();

app.run() starts the HTTP server and returns a handle:

Property / methodDescription
handle.serverThe underlying Node http.Server.
handle.stop()Graceful shutdown; returns a Promise<void>.

Options fall back to host.config.host (port, host, shutdownTimeout), then framework defaults (3000, "localhost", 10000 ms). app.run() may be called only once per app instance; a second call throws. To listen again, create a new FlareHost and call build() / run() in a fresh process or test module.

await handle.stop() (or SIGTERM / SIGINT) sets host.state to "draining", answers new requests with 503 while in-flight work finishes, runs onStop() hooks, closes the server, and sets "stopped".

During drain (after handle.stop(), SIGTERM, or SIGINT), the Node adapter rejects new inbound connections before the HTTP pipeline runs. Those requests get:

  • Status 503
  • Body { error: "Service Unavailable" }
  • Header Connection: close

In-flight requests already inside the HTTP pipeline continue until they finish or the shutdown timeout elapses. This path does not enter host.http.error handlers; it is host lifecycle, not pipeline dispatch. See HTTP errors → What bypasses handlers.

Use /ready (or similar) with host.state === "ready" if load balancers should stop sending traffic before drain begins. During "draining", new socket accepts still receive the shutdown 503 above even if a route handler would return 200.

import type { CFWExportedHandle } from "@flare-ts/core/cloudflare";
const app = host.build();
const worker: CFWExportedHandle = app.export();
export default worker;

app.export() runs startup hooks, sets host.state to "ready", and returns { fetch } for the Workers module entrypoint. Your worker receives a standard fetch(request) handler.

await app.test() returns a TestAppHandle (from @flare-ts/core/testing) with fetch(), stop(), and reset(). See Testing for the full harness API.

Calling app.test() on a production app (without FLARE_MODE=test) throws a plain Error ([flare] app.test() called on a non-test app...). Set FLARE_MODE=test in the test runner env before importing the host module instead.

host.state; // "starting" | "ready" | "draining" | "stopped"

host.state is read-only. The compiled app advances it at lifecycle transitions. Use it in readiness probes:

import { FlareHost, FlareResponse } from "@flare-ts/core";
import { node } from "@flare-ts/core/node";
const host = new FlareHost(node);
host.http.get("/ready", () => {
return host.state === "ready"
? new FlareResponse(200, { state: host.state })
: new FlareResponse(503, { state: host.state });
});
TransitionNodeWorkersTest
"starting"From construction until the server listensFrom construction until export() runsFrom construction until app.test() resolves
"ready"After the HTTP server is listeningInside export() after startupWhen app.test() finishes startup
"draining"When handle.stop() is called; new requests get shutdown 503 (see Node shutdown)N/A (per-request model)During handle.reset() only
"stopped"After server close and service teardownN/AN/A (handle.stop() does not set this)