Write your first test
Drive a Flare app through app.test() and assert on the Response, no port bound.
You want to test a route the way a request hits it in production: through routing, middleware, and the handler, asserting on the real Response. Build the host under FLARE_MODE=test, get a handle from app.test(), send a synthetic request with handle.fetch, and check the status and JSON body.
- Set
FLARE_MODE=testbefore the host module loads. - Build the app and
await app.test()to get aTestAppHandle. - Send
"METHOD /path"withhandle.fetchand assert on the returnedResponse.
import { FlareHost, FlareResponse } from "@flare-ts/core";import { node } from "@flare-ts/core/node";import { strict as assert } from "node:assert";
// FLARE_MODE=test must be set before this module loads (see Notes).const host = new FlareHost(node);host.http.get("/ping", () => new FlareResponse(200, { ok: true }));
const app = host.build();const handle = await app.test();
const res: Response = await handle.fetch("GET /ping");assert.equal(res.status, 200);assert.deepEqual(await res.json(), { ok: true });
await handle.stop();handle.fetch walks the same HTTP pipeline app.run() would, minus the socket bind. The target is a "METHOD /path" string ("GET /ping"). The method is uppercased and the path must start with /. The call resolves to a standard Web Response.
Sending a body
Section titled “Sending a body”Pass body in the second argument. Anything that is not raw bytes or a string is JSON-stringified, and content-type: application/json is set when you do not set it yourself.
import { FlareHost, FlareResponse } from "@flare-ts/core";import { node } from "@flare-ts/core/node";import { schema, str } from "@flare-ts/lib/schema";import { strict as assert } from "node:assert";
const host = new FlareHost(node);
host.http.post( "/echo", { body: schema({ name: str }) }, (_ctx, scope) => new FlareResponse(201, { greeting: `hi ${scope.input.body!.name}` }),);
const handle = await host.build().test();
const res = await handle.fetch("POST /echo", { body: { name: "ada" } });assert.equal(res.status, 201);assert.deepEqual(await res.json(), { greeting: "hi ada" });
await handle.stop();The route declares a body descriptor, so scope.input.body is typed and validated before the handler runs. A malformed payload is rejected with 400 before your handler sees it.
In a test runner
Section titled “In a test runner”Build the app once in setup, reuse the handle across cases, and call handle.stop() at the end so singleton onStop() hooks run.
// @ground:skipimport { afterAll, beforeAll, expect, it } from "vitest";import type { TestAppHandle } from "@flare-ts/core/testing";import { host } from "../src/host.js";
let handle: TestAppHandle;
beforeAll(async () => { handle = await host.build().test();});
afterAll(async () => { await handle.stop();});
it("returns ok on /ping", async () => { const res = await handle.fetch("GET /ping"); expect(res.status).toBe(200); expect(await res.json()).toEqual({ ok: true });});FLARE_MODE=testmust be set before the host module is imported. The simplest place is your runner’s env config ({ test: { env: { FLARE_MODE: "test" } } }invitest.config.ts). Without it,host.build()returns the production app andapp.test()throws.app.test()runs once per host instance. To swap a service between scenarios in one file, usehandle.reset({ replace }), not a secondapp.test().- The test path keeps middleware, error handlers, serializers, and route contracts. It only skips the socket bind and the long-lived process.
- Every
fetchresponse includes anx-request-idheader (test-1,test-2, and so on per handle).
See also
Section titled “See also”- Testing: the full
TestAppHandlesurface and@flare-ts/core/testinghelpers - Replace services in tests:
reset({ replace })between scenarios - Tutorial: Your first app: end-to-end test in a tutorial