Basilic
Architecture

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 with PGLITE=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-query hooks generated in @repo/react

How it works

  1. Author routes with TypeBox schemas (in apps/fastify/src/routes/; Fastify uses them for validation)
  2. Generate OpenAPI from route definitions
  3. 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:openapi

Generate 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 via generate-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 proxy for correct client IP behind a reverse proxy

On this page