Testing routes and server behavior
Use Brix's shared route harness to test API, page, and fragment routes through production registration, middleware, rendering, environment parsing, and error handling without starting a server.
Create the application harness
Generated and adopted applications include src/test/harness.ts. Keep createApp for normal application tests so every request receives a fresh production-configured app:
import { createTestHarness } from "brix/test";
import { HTTPException } from "brix/hono/http-exception";
import { createBrixApp } from "brix/server";
import { brixServer } from "#brix/server";
type Setup = { user?: true | { id: string; role: string }; tenant?: string };
export const { testApi, testFragment, testPage, withOptions } = createTestHarness<Setup>({
createApp: () => createBrixApp(brixServer),
env: { STAGE: "test", VERSION: "test" },
setupContext: async ({ user, tenant }) => ({
state: {
...(user ? { user: user === true ? { id: "user-1", role: "member" } : user } : {}),
...(tenant ? { tenant } : {}),
},
log: { info: () => {}, warn: () => {}, error: () => {} },
httpException: (status: number) => new HTTPException(status as 500),
}),
});
export const authenticated = withOptions({ user: true });
setupContext runs once before configured and route middleware for each request and may asynchronously create request-scoped users, tenants, or services. It supplements production middleware rather than replacing it. Harness-level middleware is available for test-only cross-cutting setup. Omit createApp only for an intentionally isolated library route.
The env snapshot is installed before production middleware and validated by the production environment parser. It never falls back to process globals, and invalid fixtures fail closed. This is the normal way to test request environment behavior; reserve brix test --env and --stage for integration tests that genuinely need configured providers.
Test an API route
import { expect, it } from "brix/test";
import { authenticated, testApi } from "#brix/test";
import { updateProfileApiRoute } from "./route.ts";
it("rejects anonymous requests", async () => {
const response = await testApi(updateProfileApiRoute, {
params: { profileId: "profile-123" },
body: { displayName: "Ada" },
});
expect(response.status).toBe(401);
expect(response.error).toEqual({ error: "Unauthorized" });
});
it("updates an authenticated profile", async () => {
const response = await authenticated.testApi(updateProfileApiRoute, {
params: { profileId: "profile-123" },
body: { displayName: "Ada" },
});
expect(response.status).toBe(200);
expect(response.expectSuccess().displayName).toBe("Ada");
});
The route retains its contract. Required bodies and exact path params are enforced by TypeScript; body and query inputs are parsed as production handlers expect. Options also include headers, cookies, locale, and request-local translations. Additional setup keys are passed to setupContext.
testApi returns status, typed successful data, parsed error, unmodified text, headers, and a clone-safe raw response. expectSuccess() throws with the status for non-2xx responses and otherwise returns the non-optional typed payload. Assert the status and error payload directly when testing application error mapping or thrown HTTPExceptions.
Test a rendered page
const response = await testPage(productDetailsPageRoute, {
locale: "sv",
params: { productId: "sku-123" },
query: { preview: true },
user: true,
});
expect(response.status).toBe(200);
expect(response.html).toContain("sku-123");
expect(response.headers.get("content-language")).toBe("sv");
The harness constructs the URL from the page contract, preserving locale, params, query serialization, route validation, global middleware, security headers, renderer, and server error handling. To test an opted-in semantic representation, pass representation: "markdown" and assert response.markdown; the response type prevents reading HTML from a Markdown result.
Test fragments and composition
Test fragment endpoint loading and transport directly:
const endpoint = await testFragment(BookingWidgetFragment, {
params: { hotelId: "hotel-1" },
pageQuery: { campaign: "summer" },
});
expect(endpoint.status).toBe(200);
expect(endpoint.html).toContain("hotel-1");
Test a page with real in-process fragment routes, or replace selected edge responses with typed deterministic mocks:
import { mockFragment } from "brix/test";
const response = await testPage(bookingPageRoute, {
query: { campaign: "summer" },
fragments: [
mockFragment(
BookingWidgetFragment,
({ params, pageQuery }) => `<aside data-mock>${params.hotelId}:${pageQuery.campaign}</aside>`,
),
],
});
Pass fragment route objects instead of mocks to exercise their real loaders. testPage sends the production defer header, renders placeholders, forwards declared page query, and runs the production composer. A mock receives only public endpoint params and forwarded query—not server-loaded props. Missing registrations, invalid transport, endpoint failures, and composition errors fail the test. Keep mockFragment in Node tests; it is not a browser component mock.
Reuse authenticated and service scenarios
withOptions binds setup values, headers, cookies, translations, or fragments without mutable globals:
const tenantAdmin = withOptions({
user: { id: "admin-1", role: "admin" },
tenant: "acme",
headers: { "x-test-suite": "tenant-admin" },
});
const response = await tenantAdmin.testApi(updateProfileApiRoute, {
params: { profileId: "profile-123" },
body: { displayName: "Ada" },
});
Per-request values override bound defaults. Headers merge case-insensitively and cookies by case-sensitive name. Contract-required bodies and params remain required. Bound harnesses expose withOptions again, so suites can derive progressively specialized auth, tenant, and request-service scenarios.
Use these tests for Brix application infrastructure. If the subject merely consumes an HTTP API, use contract-aware MSW mocks instead.