API Routes | Koze | Primitives Docs

API Routes

File-based HTTP endpoints for JSON, webhooks, resources, and agent-facing APIs

API routes are ordinary TypeScript or JavaScript modules under src/routes/api/. They are the right place for HTTP endpoints: JSON APIs, webhooks, resource downloads, upload handlers, health checks, and agent-facing tools.

Do not put API endpoints in middleware. Middleware is for cross-cutting request policy around routes; API route modules are for route-specific HTTP behavior.

File conventions

The default API root is src/routes/api, and the default URL prefix is /api. Each .ts or .js file under that root becomes one endpoint.

src/routes/api/index.ts                         -> /api
src/routes/api/v1/health/index.ts               -> /api/v1/health
src/routes/api/v1/status.ts                     -> /api/v1/status
src/routes/api/v1/platform/sites/[id]/files.ts  -> /api/v1/platform/sites/:id/files
src/routes/api/v1/r2/[bucket]/[...key]/index.ts -> /api/v1/r2/:bucket/*

Rules:

  • Only .ts and .js files are discovered as API routes.
  • File extensions are stripped from the URL.
  • Any path segment named index is dropped.
  • [id] creates a dynamic parameter.
  • [...key] creates a catch-all parameter.
  • API routes do not use .koze files, layouts, or page templates.

For example, src/routes/api/v1/filename.ts maps to /api/v1/filename. A filename.txt file is not an API route module; it will be ignored by the API route scanner.

Handler exports

Export one function per HTTP method. Supported method exports are GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS.

// src/routes/api/v1/health/index.ts
export function GET() {
  return Response.json({ ok: true });
}

Handlers return a standard Response or a promise for one.

// src/routes/api/v1/items/[id].ts
import { params } from 'koze:request';

export async function GET() {
  const item = await loadItem(params.id);
  if (!item) return Response.json({ error: 'Not found' }, { status: 404 });
  return Response.json(item);
}

export async function PATCH({ request }: { request: Request }) {
  const body = await request.json();
  const item = await updateItem(params.id, body);
  return Response.json(item);
}

If a request uses a method the route does not export, Koze returns a method error for that route instead of falling through to middleware.

Request data

API routes can use the same server-side request modules as src/server/* code:

import { request, url, headers, params, locals } from 'koze:request';
import { cookies } from 'koze:cookies';
import { env } from 'cloudflare:workers';

Use these for route-local work:

// src/routes/api/v1/webhooks/stripe.ts
import { headers } from 'koze:request';
import { env } from 'cloudflare:workers';

export async function POST({ request }: { request: Request }) {
  const signature = headers.get('stripe-signature');
  const payload = await request.text();

  await verifyAndHandleStripeEvent(payload, signature, env.STRIPE_WEBHOOK_SECRET);
  return new Response(null, { status: 204 });
}

Use locals for data middleware has already attached, such as the current user or tenant:

import { locals } from 'koze:request';

export function GET() {
  const { user } = locals as App.Locals;
  if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
  return Response.json({ user });
}

Route metadata

API route modules may export a manifest object. Koze merges it with framework metadata such as route kind, file, pattern, and exported methods. Use this for documentation, agent discovery, or internal tooling.

export const manifest = {
  summary: 'List platform sites',
  auth: 'required',
  tags: ['platform', 'sites'],
};

export async function GET() {
  return Response.json(await listSites());
}

For Cloudflare API Shield, prefer a route-adjacent *.api-shield.ts sidecar such as index.api-shield.ts. Koze uses that file to generate _cloudflare/api-shield/openapi.json. Legacy static manifest fields still feed the same artifact for compatibility. See API Shield for schema validation setup.

Middleware boundary

Middleware should stay small and general:

  • authenticate or attach request-scoped data to ctx.locals
  • apply rate limits or security headers
  • route a whole protocol or hostname to another handler
  • log, trace, or transform responses

API routes should own endpoint behavior:

  • validate request bodies
  • choose response status codes and JSON shapes
  • call database, KV, R2, Durable Object, or service modules
  • implement webhooks, uploads, health checks, and agent-visible endpoints

This split keeps middleware readable and prevents large if pathname.startsWith switchboards from becoming the application router.

Custom API root or prefix

The compiler defaults are equivalent to:

// vite.config.ts
koze({
  api: {
    root: 'src/routes/api',
    urlPrefix: '/api',
  },
});

Change these only when the project needs a different directory or public URL prefix. The same file rules still apply.

  • Routing - page routing and layout conventions
  • Middleware - cross-cutting request lifecycle hooks
  • Request APIs - request context, cookies, and env