Package Conventions
Monorepo package architecture conventions: core / react with clean boundaries, no duplication, and portable design.
Overview
This document defines conventions for a TypeScript + ESM monorepo with:
- Fastify + TypeBox for HTTP + schemas
- OpenAPI generated from route schemas
- @hey-api/openapi-ts for the generated TypeScript client (in
@repo/core) - Zod for generated schemas and runtime validation (when needed)
- TanStack Query for React async state (in
@repo/react)
Goals: clean boundaries, no duplicated sources of truth, portable packages, and a fast DX in a pnpm workspace.
Why this architecture
We want three outcomes at the same time:
- Single source of truth for API shapes (request/response) with runtime validation.
- Apps across the monorepo can import types safely without dragging server/runtime dependencies everywhere.
- Packages compile to clean ESM artifacts for npm (but we can still do fast local dev in the monorepo).
The key idea:
- Fastify routes + TypeBox schemas are the source of truth.
- The OpenAPI spec is generated from routes.
@repo/corecontains the generated client + types and a small wrapper API.@repo/reactis a React Query layer on top of@repo/core(handwritten hooks/components).
Workspace layout
This repository is a pnpm workspace with these top-level groups:
apps/ # product apps (Next.js, Fastify, docs site)
packages/ # shared libraries published as @repo/*
tools/ # shared tooling (eslint, typescript config, etc.)Package layout
packages/
core/ # generated client + types (runtime-agnostic)
react/ # TanStack Query hooks + React utilities (built on core)
apps/
fastify/ # Fastify server + TypeBox routes → generates OpenAPI
next/ # Next.js app consuming core/reactDependency direction (strict)
@repo/core→ owns generated code, exports API types@repo/react→ depends on@repo/core(and React/TanStack Query via peer deps)
No reverse dependencies.
Mermaid architecture diagram
Package responsibilities (high level)
1) API contracts (TypeBox → OpenAPI)
The HTTP boundary is defined by Fastify route schemas (TypeBox). The OpenAPI spec is a generated artifact.
What goes here
- Route schemas + handlers in
apps/fastify/src/routes/ - Generated OpenAPI 3.0 file at
apps/fastify/openapi/openapi.json(do not hand-edit) - Served OpenAPI JSON at
/reference/openapi.jsonand Scalar UI at/reference
See OpenAPI Generation for the exact workflow.
2) packages/core
The runtime-agnostic client package with generated OpenAPI client code and types.
What goes here
- Generated TypeScript client code from the OpenAPI spec (via
@hey-api/openapi-ts) - Types and Zod schemas generated from OpenAPI
createClient()andcreateApi()wrappers (seepackages/core/README.md)
Hard rules
- ✅ No React
- ✅ No TanStack Query
- ✅ Generated code lives in
packages/core/src/gen/ - ✅ All OpenAPI types are re-exported from
@repo/core(viaexport type * from './gen/types.gen')
3) packages/react
React-only helpers using TanStack Query.
What goes here
- React Query hooks that call
@repo/core(handwritten) - Shared provider/context to supply a core client instance to hooks
- Small React utilities/components that are reusable across apps
Hard rules
- ✅ React-only dependencies live here
- ✅
@tanstack/react-queryis a peer dependency (apps own the version)
Server implementation generates OpenAPI
The server app (apps/fastify) should treat Fastify routes + TypeBox schemas as the source of truth.
- Define request/response schemas in the route (TypeBox)
- Generate OpenAPI from routes into
apps/fastify/openapi/openapi.json - Expose OpenAPI JSON at
/reference/openapi.json - Mount Scalar UI at
/reference
Import and export conventions
Use only public entrypoints
- Import from the package root or an exported subpath.
- Never deep-import internal files (e.g.
@repo/core/src/...). - See Packages Reference for the canonical list of entrypoints.
For @repo/utils, prefer subpath imports even though a root barrel exists:
import { delay } from '@repo/utils/async'
import { logger } from '@repo/utils/logger/server'
import { getErrorMessage } from '@repo/utils/error'Dependency management strategy
Pick dependency types based on who should own the version:
- App/framework deps → peerDependencies: React, React DOM, TanStack Query, Next.js, etc.
- Tightly-coupled internal deps → dependencies: shared UI building blocks, generated code wrappers.
- Optional integrations → optional peerDependencies: e.g. Sentry SDKs.
Rule of thumb:
If the consumer must control it → peerDependency
If the library cannot function without it → dependency
If it is used only in dev/test/build → devDependencyAlso: list peer deps in devDependencies too so the package type-checks and tests in isolation.
Adding a new API endpoint (checklist)
When you add or change an endpoint:
- Add/update the Fastify route + TypeBox schemas in
apps/fastify/src/routes/** - Regenerate OpenAPI:
pnpm --filter @repo/fastify generate:openapi - Regenerate the core client:
pnpm --filter @repo/core generate - If you want a React Query wrapper, add/update a hook in
packages/react/src/hooks/ - Consume the types from
@repo/core(single source of truth)
Multi-language SDK philosophy
TypeScript consumers use generated clients from @hey-api/openapi-ts for full type safety.
Other languages (Go, Rust, Python) generate SDKs from the same OpenAPI spec when needed.
This gives us:
- ✅ Generated TypeScript clients with full type safety
- ✅ Consistent API contracts across all languages
- ✅ OpenAPI always available for non-TS consumers
See:
- API Architecture for the end-to-end flow (TypeBox → OpenAPI → clients/hooks)
- OpenAPI Generation for the exact commands and generated output locations
ESM & TypeScript strategy
This monorepo is ESM-first and uses source exports in-workspace with a publish switch to dist/ (plus subpath exports and runtime-aware config for Next.js vs Node).
All the details (export maps, prepack/postpack, subpath exports, Next.js transpilePackages, Node dev with tsx, TypeScript module resolution) live in:
Summary
- Fastify routes + TypeBox schemas are the source of truth; OpenAPI is generated from routes.
@repo/coreowns generated client code + types; apps import types from it.@repo/reactis the React Query layer built on top of@repo/core.- Prefer subpath exports when available; never deep-import internal files.
- ESM everywhere (see ESM & TypeScript Strategy).