Web APIs

Authorize next to the data.

Turtle route handlers use standard Request and Response objects. Every endpoint must export a complete policy; application code owns authorization, validation, logging, and safe projection.

Route handlers

// app/api/items/[id]/route.ts
import type { EndpointPolicy } from "turtle";
import type { RouteHandlerContext } from "turtle/runtime";

export const policy = {
  session: "required",
  verification: "required",
  subscription: "none",
  admin: "none",
  rateLimit: "standard",
} as const satisfies EndpointPolicy;

export async function GET(request: Request, { params }: RouteHandlerContext) {
  const { id } = await params;
  const item = await loadAuthorizedItem(id, request);
  return Response.json({ id: item.id, title: item.title });
}

The scanner refuses handlers without a policy export. Supported methods are GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. HEAD falls back to GET with an empty body. OPTIONS is generated automatically with Allow; unsupported methods return 405.

Validate at the boundary

Path parameters are decoded and validated by the router. JSON bodies, query semantics, headers from outside systems, and webhook payloads remain application boundaries and must be parsed with runtime schemas before use. Return structured safe errors as { code, title, message }; send details only to server logs.

Request-local context

import { after, connection, cookies, headers } from "turtle/runtime";

const requestId = headers().get("x-request-id");
const session = cookies().get("session");
await connection();
after(async () => recordBestEffortMetric(requestId));
  • headers() returns a copy of current request headers.
  • cookies() returns an immutable cookie-name map parsed once at the request boundary.
  • connection() explicitly marks the current render dynamic.
  • after() records lazy request-owned work. The host adapter schedules it after the response; it is best-effort and may be lost.

Context uses Bun AsyncLocalStorage and remains isolated across concurrent streams. Calling these APIs outside a request throws.

Application-owned RPC

Turtle does not prescribe a transport library. The conformance application demonstrates a typed proxy client sending { procedure, payload } to one policy-declared handler, with Zod schemas validating the envelope and procedure input. Keep protocol-only types in a shared module, execution behind server-only, and fetch code behind client-only.

Background-work semantics

The adapter decides how tasks run. Turtle's local server begins each task and tracks it; the default in-process adapter logs rejected work. Critical work must be awaited in the request and designed idempotently. Webhooks and retried mutations need stable external IDs and database unique keys—not read-before-write checks.