Skip to content

ADR 007 Project Structure

Claude product-architect (Opus 4.6) edited this page Feb 22, 2026 · 2 revisions

ADR-007: Project Structure

Status

Accepted

Context

Cornerstone is a full-stack TypeScript application with a React client, Fastify server, and shared type definitions. The project needs a structure that:

  • Keeps client and server code separated but in one repository
  • Shares TypeScript types between client and server
  • Supports independent build steps for each package
  • Works with npm's native workspace feature (no additional monorepo tooling)
  • Keeps the structure simple for a small team (<5 users, <3 developers)

Alternatives Considered

Single-package structure (flat)

  • All code in one src/ directory with client/ and server/ subdirectories
  • Simplest setup but makes independent builds difficult
  • Shared types require import path conventions rather than package boundaries
  • Build configuration becomes complex (one tsconfig handling both client and server)

Turborepo / Nx monorepo

  • Powerful build orchestration with caching and parallel execution
  • Additional tooling dependency and configuration complexity
  • Designed for large monorepos with many packages -- overkill for 3 packages
  • Adds a learning curve for new contributors

npm workspaces (native)

  • Built into npm; no additional tooling required
  • Supports npm run <script> -w <workspace> for targeted execution
  • Workspace packages reference each other via "*" version specifier
  • Simple, well-understood, and sufficient for 3 packages

Decision

Use npm workspaces with three packages: shared, server, and client.

Directory Layout

cornerstone/
  package.json              # Root workspace config, shared dev dependencies
  tsconfig.base.json        # Base TypeScript config (extended by each package)
  eslint.config.js          # ESLint flat config (shared across all packages)
  .prettierrc               # Prettier config
  vitest.config.ts          # Vitest config (discovers tests across all packages)
  Dockerfile                # Multi-stage Docker build
  CLAUDE.md                 # Project guide and conventions
  .gitignore
  .dockerignore
  shared/                   # @cornerstone/shared
    package.json
    tsconfig.json
    src/
      types/                # API request/response types, entity types
      index.ts              # Package entry point (re-exports)
  server/                   # @cornerstone/server
    package.json
    tsconfig.json
    drizzle.config.ts       # Drizzle-kit configuration
    src/
      app.ts                # Fastify app factory
      server.ts             # Entry point (starts HTTP server)
      routes/               # Route handlers organized by domain
      plugins/              # Fastify plugins (auth, database, etc.)
      services/             # Business logic layer
      db/
        schema.ts           # Drizzle ORM schema definitions
        migrations/         # Generated SQL migration files
      types/                # Server-only types
  client/                   # @cornerstone/client
    package.json
    tsconfig.json
    vite.config.ts          # Vite configuration
    index.html              # SPA entry HTML
    src/
      main.tsx              # Application entry point
      App.tsx               # Root component with routing
      components/           # Reusable UI components
      pages/                # Route-level page components
      hooks/                # Custom React hooks
      lib/                  # Utility functions, API client
      styles/               # Global styles (Tailwind entry point)

Package Dependencies

@cornerstone/shared  <--  @cornerstone/server
                     <--  @cornerstone/client

Both server and client depend on shared. The shared package has no dependencies on either.

Build Order

  1. shared (tsc) -- must be built first since others depend on it
  2. client (vite build) -- produces static assets in client/dist/
  3. server (tsc) -- produces Node.js modules in server/dist/

The root build script enforces this order:

npm run build -w shared && npm run build -w client && npm run build -w server

Naming Conventions

Context Convention Example
Database columns snake_case created_at, budget_category_id
TypeScript variables/functions camelCase createdAt, getBudgetCategory
TypeScript types/interfaces PascalCase WorkItem, BudgetCategory
File names (TS) camelCase workItem.ts, budgetService.ts
File names (React components) PascalCase WorkItemCard.tsx, GanttChart.tsx
API endpoints kebab-case /api/work-items, /api/budget-categories
CSS class names Tailwind utilities (no custom BEM/class conventions)

Consequences

Positive

  • No additional monorepo tooling beyond npm
  • Clear separation of concerns between client, server, and shared code
  • TypeScript project references ensure correct build ordering
  • Each package can have its own tsconfig tuned to its environment (browser vs Node.js)
  • Simple enough for a small team to understand immediately

Negative

  • npm workspaces lack build caching (every npm run build rebuilds from scratch)
  • No parallel build orchestration (sequential build order required)
  • Adding a fourth package (e.g., e2e tests) requires updating the root workspace config
  • Shared package must be built before running server or client in development (handled by tsx/vite resolving source directly)

Clone this wiki locally