lilith-platform.live/codebase/@features/api/ONBOARDING.md

83 lines
3.3 KiB
Markdown

# quinn.api — Contributor Guide
## Where does X go?
| Trigger | Layer | Path |
|---------|-------|------|
| New DB table / domain noun | `entities/<noun>/` | types, schema, repo, barrel |
| CRUD endpoint for one entity | `surfaces/<surface>/<noun>.ts` | import from entity barrel |
| Logic touching 2+ entities | `features/<verb>/` | 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/<noun>/
├─ Route exposing entity CRUD? → surfaces/<surface>/<noun>.ts
├─ Orchestrates multiple entities? → features/<verb>/
└─ Framework primitive? → shared/<concern>/
```
## How to add an entity
```bash
bun run scaffold:entity <name> # kebab-case, e.g. financial-record
```
This generates four files under `src/entities/<name>/`:
- `types.ts` — domain interfaces and const arrays
- `schema.ts``<name>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 `<name>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/<entity>.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/<surface>/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.