Skip to content

Route groups and versioning

Register v1 and v2 routes under shared prefixes with host.http.group and per-group middleware, CORS, and errors.

AI generated, pending review Updated 10 days ago · Flare 0.3

You’re shipping a /v2 of your API while /v1 stays live, and you don’t want to repeat the prefix on every route. Put each version in its own host.http.group(prefix, …). The prefix lives in one place, and routes inside read like they always do.

  1. Call host.http.group("/v1", (g) => { … }) and register that version’s routes on g.
  2. Do the same for /v2 in a second group.
  3. Return g.register() from each builder callback. That call finalizes the group and is required.
import { FlareHost, FlareResponse } from "@flare-ts/core";
import { node } from "@flare-ts/core/node";
const host = new FlareHost(node);
host.http.group("/v1", (g) => {
g.get("/health", () => new FlareResponse(200, { version: "v1" })); // GET /v1/health
g.get("/users", () => new FlareResponse(200, { users: [] })); // GET /v1/users
return g.register();
});
host.http.group("/v2", (g) => {
g.get("/health", () => new FlareResponse(200, { version: "v2" })); // GET /v2/health
g.get("/users", () => new FlareResponse(200, { users: [] })); // GET /v2/users
return g.register();
});
const app = host.build();
app.run();

Every path registered on g is prefixed with the group’s prefix by string concatenation: g.get("/users", …) under /v2 mounts at /v2/users.

import { ControllerBase, FlareHost, httpContract } from "@flare-ts/core";
import { Get } from "@flare-ts/core/decorators";
import { node } from "@flare-ts/core/node";
import { int } from "@flare-ts/lib/schema";
const UsersContract = httpContract({
show: { route: { id: int } },
});
class UsersV1Controller extends ControllerBase {
public static override deps = [];
public static override state = [];
public static override contract = UsersContract;
@Get("/:id")
show() {
const { route } = this.ctx.extract(UsersContract.show);
return this.ok({ id: route.id, version: "v1" });
}
}
class UsersV2Controller extends ControllerBase {
public static override deps = [];
public static override state = [];
public static override contract = UsersContract;
@Get("/:id")
show() {
const { route } = this.ctx.extract(UsersContract.show);
return this.ok({ id: route.id, version: "v2" });
}
}
const host = new FlareHost(node);
host.http.group("/v1", (g) => {
g.controller("/users", UsersV1Controller); // GET /v1/users/:id
return g.register();
});
host.http.group("/v2", (g) => {
g.controller("/users", UsersV2Controller); // GET /v2/users/:id
return g.register();
});
const app = host.build();
app.run();

The builder g exposes the same registration surface as host.http, scoped to the group:

import { FlareHost, FlareResponse, MiddlewareBase } from "@flare-ts/core";
import { node } from "@flare-ts/core/node";
class LegacyAuthMiddleware extends MiddlewareBase {
public static override deps = [];
public static override state = [];
before() {
if (!this.ctx.req.headers.get("authorization")) {
return this.unauthorized({ error: "missing token" });
}
}
}
const host = new FlareHost(node);
host.http.group("/v1", (g) => {
g.use(LegacyAuthMiddleware); // runs only for /v1/* routes
g.cors({ origins: ["https://app.example.com"], credentials: true });
g.get("/users", () => new FlareResponse(200, { users: [] }));
return g.register();
});
const app = host.build();
app.run();

g.cors() fully replaces the arc-level CORS policy for routes in that group. It does not merge with host.http.cors().

  • Groups don’t nest. Use one group with a combined prefix (for example /api/v1) instead of stacking groups.
  • Prefix join is literal. Both the group prefix and route paths must be valid absolute segments (/api/v1 + /users/api/v1/users).
  • g.isolated() skips global middleware for routes in the group. Register what you need with g.use or group-scoped hooks instead.