OpenAPI pipeline

apps/backend/openapi/openapi.yaml is the durable source of truth for Clay’s HTTP surface. It is regenerated from the live Hono app, linted by Redocly, and consumed by Fern to produce @clay/client.

Purpose

The pipeline keeps the OpenAPI document in lockstep with the implemented routes so that the TypeScript client never drifts from what the Worker actually serves. Treat the YAML as a generated artifact and the Zod schemas in src/index.ts as the single source.
Never hand-edit apps/backend/openapi/openapi.yaml. Regenerate it from the Hono app so the contract always reflects the implemented routes.

Pipeline

1

Hono route schemas

Each route is declared with createRoute from @hono/zod-openapi. Request and response shapes are described with Zod, then attached to the route’s responses map.
2

OpenAPI 3.1 document builder

The Hono app exposes the document through getOpenApiDocument(), which calls app.getOpenAPI31Document(openApiDocumentConfig). The same document is served live at GET /openapi.json via app.doc31(...) for ad-hoc debugging.
3

Export script

scripts/export-openapi.ts imports getOpenApiDocument(), YAML-stringifies the result with yaml, and writes it to openapi/openapi.yaml. It uses tsx to execute TypeScript without a build step.
4

Redocly lint

openapi:check runs redocly lint against the generated YAML to enforce structure, naming, and operation completeness.
5

Fern consumer

packages/client/fern/generators.yml references the YAML and runs the fernapi/fern-typescript-sdk generator under the ts-sdk group. The SDK lands in packages/client/src/generated/ and is the type-only entry point every app imports from.

Source schemas

The current src/index.ts registers three Zod schemas and two routes. Every schema declared with .openapi("<Name>") becomes a component in the generated YAML.

RootResponseSchema

Inline z.string() for the GET / plain-text body. Annotated with .openapi({ example: "Hello, world!" }) for inline metadata, but not named — emitted inline under the response schema.

HealthResponse

{ status: "ok", service: "clay-backend" } object schema. Registered for reuse across the health surface.

ErrorResponse

{ error: string } object schema. Attached to the 404 response on both routes and to the notFound handler.
const RootResponseSchema = z.string().openapi({ example: "Hello, world!" });

const HealthResponseSchema = z
.object({
status: z.literal("ok"),
service: z.literal("clay-backend"),
})
.openapi("HealthResponse");

const ErrorResponseSchema = z
.object({
error: z.string(),
})
.openapi("ErrorResponse");

Export step

The export script imports the live Hono app in-process and writes the OpenAPI 3.1.0 document to disk. It depends on yaml for serialization and on tsx for TypeScript execution without a build step.
import { mkdir, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import YAML from "yaml";
import { getOpenApiDocument } from "../src/index.js";

const outputYamlPath = resolve("openapi", "openapi.yaml");
const openApiDocument = getOpenApiDocument();
const openApiYaml = YAML.stringify(openApiDocument);

await mkdir(resolve("openapi"), { recursive: true });
await writeFile(outputYamlPath, openApiYaml);

process.stdout.write(`Wrote ${outputYamlPath}\n`);

The generate script is an alias for openapi:export, which runs the script above with tsx. The Worker also exposes the same document at runtime through app.doc31("/openapi.json", openApiDocumentConfig), which is useful for live debugging without rebuilding.

Validation step

Redocly CLI lints the generated YAML against its built-in OpenAPI ruleset. Run it after every schema or route change so the downstream Fern generator never receives a malformed contract.
pnpm --filter @clay/backend openapi:check
The lint script delegates to openapi:check so the standard turbo lint task also enforces OpenAPI health at the workspace level.

Downstream consumer

Fern reads apps/backend/openapi/openapi.yaml and generates the typed @clay/client package. The client is what apps/mobile and any future Clay consumer calls — never the raw HTTP surface.

OpenAPI YAML

apps/backend/openapi/openapi.yaml — generated contract.

Fern generator

fernapi/fern-typescript-sdk@3.72.5 with fetchSupport: native, wired by the ts-sdk group in packages/client/fern/generators.yml.

@clay/client

packages/client/ — the typed TypeScript package shipped to apps.

Regenerate commands

TaskCommand
Regenerate the OpenAPI YAMLpnpm --filter @clay/backend generate
Lint the generated YAMLpnpm --filter @clay/backend openapi:check
Typecheck the backendpnpm --filter @clay/backend typecheck
Build the backend (typecheck)pnpm --filter @clay/backend build
Regenerate the SDKpnpm --filter @clay/client generate
Validate Fern configpnpm --filter @clay/client check
Wipe the generated artifactpnpm --filter @clay/backend clean

Drift rules

1

Schemas live next to handlers

Define route schemas with Zod in the same file as the handler that serves the route. Avoid a separate schemas/ folder until the surface grows.
2

Reuse shared shapes

Register cross-route shapes with .openapi("<Name>") so they appear under components.schemas. Inline Zod chains stay inline for single-use shapes.
3

Lint after every change

Run openapi:check after touching a route, a schema, or the export script. The lint is the first gate before Fern regenerates @clay/client.
4

Never hand-edit the YAML

If the YAML disagrees with the code, the code is the source of truth. Regenerate rather than patch.
5

Land backend and SDK changes together

When a backend schema changes, regenerate openapi/openapi.yaml and @clay/client in the same change. Turbo’s per-package overrides (@clay/client#generate depends on @clay/backend#generate) make this the default when the cache is fresh.

Acceptance criteria

The YAML regenerates cleanly with pnpm --filter @clay/backend generate.
Redocly reports zero errors with pnpm --filter @clay/backend openapi:check.
Every registered .openapi("<Name>") schema appears under components.schemas.
Every createRoute operation has a matching entry under paths.
Fern can consume the YAML and produce @clay/client without manual fixes.
The runtime /openapi.json document and the exported YAML agree on every operation.

Backend overview

The Hono app and Cloudflare Worker this pipeline describes.

API reference

The implemented routes and Zod schemas behind the contract.

Environment and bindings

The Wrangler bindings and runtime secrets the Worker exposes.

@clay/client package

The Fern-generated TypeScript SDK consumed by apps.

Architecture

Where this pipeline sits inside the Turborepo surface layout.