Kizaki
ReferenceInspire

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.params for path parameters like :projectId
  • req.query for simple query-string access
  • req.headers for auth signatures and integration metadata
  • req.rawBody for webhook signature verification
  • req.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.

  • keep route declarations short and structural
  • group by path prefix and auth model
  • make apiKey(scope) the default for machine access
  • reserve none for webhook-style verification flows
  • keep heavy business logic in reusable server code, not inline in handlers

Related docs:

On this page