API Architecture
Single-source, type-safe REST APIs: Fastify routes (TypeBox) generate OpenAPI, which generates typed clients and React Query hooks.
We treat Fastify routes + TypeBox schemas as the single source of truth for both:
- Runtime behavior: request/response validation in Fastify
- Tooling: OpenAPI spec generation, then typed clients/hooks generation
Data flow: TypeBox (backend) → OpenAPI spec → generated types & Zod schemas → generated clients & React Query hooks.
Principles
- Single source of truth: schemas live with the route (no parallel hand-written spec)
- Runtime validation by default: every route defines request/response schemas
- Generated consumers: clients/hooks are generated from the route-derived OpenAPI spec
Stack
- Runtime: Node.js LTS — stability, ecosystem, tooling
- Framework: Fastify — performance, TypeBox, plugin-based, ESM-native
- Database: PostgreSQL (any host via
DATABASE_URL; PGLite for tests withPGLITE=true) - ORM: Drizzle — type-safe queries, SQL-like syntax, no code generation
- Spec: OpenAPI 3 — standard interoperability for docs/tooling/SDKs
- Client generation:
@hey-api/openapi-ts— typed TS client + Zod schemas - React integration:
@tanstack/react-queryhooks generated in@repo/react
How it works
- Author routes with TypeBox schemas (in
apps/fastify/src/routes/; Fastify uses them for validation) - Generate OpenAPI from route definitions
- Generate SDKs from OpenAPI (TypeScript client + Zod schemas + React Query hooks)
At runtime, the request path is: request → Fastify → schema validation → handler → Drizzle queries → typed JSON response.
Example
Route schema (Fastify)
import { Type } from '@sinclair/typebox'
const HealthResponseSchema = Type.Object({
ok: Type.Literal(true),
})
fastify.get('/health', {
schema: {
response: {
200: HealthResponseSchema,
},
},
}, async (request, reply) => {
return reply.send({
ok: true,
})
})Generate OpenAPI
The OpenAPI spec is generated from route definitions and written to apps/fastify/openapi/openapi.json.
pnpm --filter @repo/fastify generate:openapiGenerate TypeScript client
Hey API generates type-safe clients from the OpenAPI spec (generated output lives in packages/core; do not hand-edit):
// Generated by @hey-api/openapi-ts
import { createClient } from './gen/client'
const client = createClient({
baseUrl: 'https://api.example.com',
})Usage (Next.js)
import { createApi } from '@repo/core'
import { useHealthCheck } from '@repo/react'
const api = createApi({
baseUrl: process.env.NEXT_PUBLIC_API_URL!,
getAuthToken: async () => session?.token,
})
const health = await api.healthCheck()
const { data } = useHealthCheck()OpenAPI Integration
- Generation:
apps/fastify/openapi/openapi.json(generated from routes viagenerate-openapi.ts) - Serving:
/reference/openapi.json(programmatic access) - Docs UI:
/reference(Scalar) - AI tooling: MCP integration can consume the spec for agent workflows
SDK Generation
TypeScript is generated by @hey-api/openapi-ts. Other languages can use standard OpenAPI generators (for example openapi-generator, oapi-codegen, progenitor). All SDKs are generated from the same OpenAPI spec produced from routes.
Database
The API layer uses Drizzle for queries and drizzle-kit for migrations. Migration strategy depends on the database runtime; see ADR 008: Database Platform & Strategy.
Security
- Headers: X-Content-Type-Options, X-Frame-Options, CSP, HSTS
- CORS: configurable origin restrictions
- Rate limiting: per-IP
- Validation & observability: schema validation on requests/responses, security events logged,
trust proxyfor correct client IP behind a reverse proxy