# quinn.api — Contributor Guide ## Where does X go? | Trigger | Layer | Path | |---------|-------|------| | New DB table / domain noun | `entities//` | types, schema, repo, barrel | | CRUD endpoint for one entity | `surfaces//.ts` | import from entity barrel | | Logic touching 2+ entities | `features//` | e.g. `tour-planning`, `client-intake` | | Auth, CORS, rate-limit, error shape | `app/middleware/` | applied in `app/server.ts` | | DB connection, migrations runner | `shared/db/` | no other layer owns this | | HTTP error helpers, fetch utils | `shared/http/` | | | Mail, iCal, TOTP primitives | `shared/mail/`, `shared/ical/`, `shared/totp/` | | Decision tree: ``` New work incoming ├─ Single noun, single table? → entities// ├─ Route exposing entity CRUD? → surfaces//.ts ├─ Orchestrates multiple entities? → features// └─ Framework primitive? → shared// ``` ## How to add an entity ```bash bun run scaffold:entity # kebab-case, e.g. financial-record ``` This generates four files under `src/entities//`: - `types.ts` — domain interfaces and const arrays - `schema.ts` — `Migrations: readonly Migration[]` - `repo.ts` — `list/get/create/update/delete` functions (no ORM) - `index.ts` — barrel re-export After scaffolding: 1. Replace the placeholder types with your real domain model. 2. Add `Migrations` to the `runMigrations(...)` call in `src/app/server.ts`. 3. Mount the entity in a surface (see next section). ## How to add a surface route ```ts // src/surfaces/my/.ts import { Hono } from 'hono'; import { listFoos, createFoo } from '@/entities/foo'; import { getDb } from '@/shared/db'; export const foosRouter = new Hono() .get('/', (c) => c.json(listFoos(getDb()))) .post('/', async (c) => { const body = draftSchema.parse(await c.req.json()); return c.json(createFoo(getDb(), body), 201); }); ``` Then mount it in `src/surfaces//index.ts`: ```ts export const mySurface = new Hono() .route('/foos', foosRouter); ``` Surfaces are thin. Keep each file under 80 lines. Validation lives in the surface; business logic lives in `features/`; DB access lives in `entities/`. ## Dependency rules `shared` imports nothing outside itself. `entities` imports `shared` and other entities (via barrel only — never reach into another entity's internals). `features` imports `entities` and `shared`, never other features or surfaces. `surfaces` imports `features`, `entities`, and `shared`, never another surface. `app` imports everything. These rules are enforced by `dependency-cruiser` in CI — a failing lint blocks merge. ## Running tests ```bash bun test # all tests bun test src/__tests__/server # specific file ``` Tests use an in-memory SQLite via `createTestApp()` from `src/__tests__/harness.ts`. No real files are touched. Each test file calls `createTestApp()` in `beforeEach` and `ctx.close()` in `afterEach` to reset state between tests. ## Proposing a dependency-cruiser exception If a boundary rule feels wrong for a legitimate reason, open a PR editing `.dependency-cruiser.cjs` with an inline comment explaining why the boundary should bend. Three or more exceptions filed in a quarter is a signal to revisit the rules rather than accumulate exceptions.