Routes
Declare HTTP endpoints in Inspire and route requests into server handlers.
Routes define the HTTP surface of your app for callers outside the normal generated-client path. They are the right tool for webhooks, partner integrations, machine-to-machine APIs, and any external system that should talk to your app over plain HTTP.
For your own frontend, exposed functions plus @kizaki/client are still the better default. Routes are the escape hatch when the caller is not your generated client.
Basic Shape
routes("/v1", auth: apiKey(read)) {
GET /projects -> listProjectsRoute
POST /projects -> createProjectRoute
}A route declaration answers four questions in one place:
- what path exists
- which HTTP method it accepts
- which handler owns it
- how callers are authenticated
That keeps your external API surface visible in the schema instead of spreading it across ad hoc server files.
Grouping, Prefixes, And Overrides
Route groups let you share a prefix and default settings across many endpoints.
routes("/v1", auth: apiKey(read), rateLimit: 1000/minute) {
GET /projects -> listProjectsRoute
GET /projects/:projectId -> getProjectRoute
POST /projects -> createProjectRoute (auth: apiKey(write))
DELETE /projects/:projectId -> deleteProjectRoute (auth: apiKey(write))
routes("/admin", auth: apiKey(admin)) {
GET /users -> listUsersRoute
DELETE /users/:userId -> deleteUserRoute
}
}The important rule is simple: group defaults keep broad policy readable, and per-route overrides only appear where behavior really changes.
Auth Modes
Routes support three auth modes.
session
Use session when the caller is a signed-in user and the route should behave like the rest of your app.
routes("/app", auth: session) {
GET /me -> getCurrentUserRoute
POST /projects -> createProjectRoute
}This is appropriate when you need HTTP semantics directly, but still want normal user identity, policies, and tenant scoping.
apiKey(scope)
Use apiKey(scope) for machine-to-machine access.
routes("/v1", auth: apiKey(read)) {
GET /projects -> listProjectsRoute
GET /projects/:projectId -> getProjectRoute
}The scope name comes from the apiKeys block in your schema. This is the normal choice for partner APIs, ingestion endpoints, and automation that should not rely on user sessions.
none
Use none only when the route has its own verification model, such as a signed webhook.
routes("/webhooks", auth: none) {
POST /stripe -> stripeWebhookRoute
}If a route is publicly reachable without a session or API key, the handler should have a very clear reason for existing and should verify the caller in some other way.
Handler Model
The schema owns the routing table. Your server code owns the workflow inside each handler.
A typical handler reads the request, calls normal SDK queries or workflows, and returns an HTTP response with the SDK helpers.
import { Project } from "@kizaki/schema";
import {
RouteRequest,
badRequest,
created,
insert,
ok,
query,
select,
} from "@kizaki/sdk";
export async function listProjectsRoute(_req: RouteRequest) {
const rows = await query(
select(Project)
.fields(Project.id, Project.name)
.orderBy("createdAt", "desc"),
);
return ok(rows);
}
export async function createProjectRoute(req: RouteRequest) {
const input = req.body<{ name?: string }>();
if (!input?.name) {
return badRequest("name is required");
}
const [project] = await query(
insert(Project)
.values({ name: input.name })
.returning(),
);
return created(project);
}The main design point is that routes are thin HTTP boundaries. They should translate HTTP concerns into application workflows, not become a second copy of your business logic.
Request Surface
Handlers receive a typed request object with the parts you usually need for HTTP integration work.
export async function getProjectRoute(req: RouteRequest) {
const projectId = req.params.projectId;
const format = req.query.format;
const method = req.method;
const path = req.path;
const requestId = req.headers["x-request-id"];
const rawBody = req.rawBody;
const body = req.body<{ includeArchived?: boolean }>();
return ok({ projectId, format, method, path, requestId, rawBodyLength: rawBody.length, body });
}The most important parts in practice are:
req.paramsfor path parameters like:projectIdreq.queryfor simple query-string accessreq.headersfor auth signatures and integration metadatareq.rawBodyfor webhook signature verificationreq.body<T>()for JSON request payloads
Response Helpers
Use the small response helper surface from @kizaki/sdk rather than building HTTP responses by hand.
import {
badRequest,
created,
forbidden,
httpError,
noContent,
notFound,
ok,
redirect,
} from "@kizaki/sdk";
ok({ id: "p_123" });
created({ id: "p_123" });
noContent();
redirect("/login");
badRequest("invalid payload");
forbidden("missing scope");
notFound("project not found");
httpError(422, "unprocessable request");This keeps handler code predictable and makes route behavior easy to scan.
Webhooks
Webhook routes are the main case for auth: none.
routes("/webhooks", auth: none) {
POST /github -> githubWebhookRoute
}import { RouteRequest, badRequest, ok } from "@kizaki/sdk";
export async function githubWebhookRoute(req: RouteRequest) {
const signature = req.headers["x-hub-signature-256"];
if (!signature) {
return badRequest("missing signature");
}
// Verify req.rawBody with the provider SDK or shared helper here.
// Then hand off to normal application logic.
return ok({ received: true });
}The route is public at the HTTP layer, but the verification still happens in your handler logic.
Routes And The Rest Of Your App
Routes do not replace the normal Kizaki workflow split.
- use routes for external HTTP entrypoints
- use exposed functions for app-owned workflows
- keep reads and writes on normal entities and policies
- let API-key scopes decide which machine callers exist
If you find yourself turning your whole app into routes, that is usually a sign you should be using exposed functions and the generated client instead.
Recommended Pattern
- keep route declarations short and structural
- group by path prefix and auth model
- make
apiKey(scope)the default for machine access - reserve
nonefor webhook-style verification flows - keep heavy business logic in reusable server code, not inline in handlers
Related docs: