api — agent rules¶
Always-on guidance for the Bricks API. Loads for Claude Code (via the projects/api/CLAUDE.md symlink) and Cursor (glob-scoped).
Canonical source is this file at
projects/api/.cursor/rules/.projects/api/CLAUDE.mdand everything underprojects/api/.claude/are symlinks — edit the original, not the symlink.Root rules apply. See
claude-root.mdc. This file only states the API-specific delta.
Topical rules¶
- API conventions — layered architecture, DO/DO NOT,
Resultpattern, Kysely+Zod stacks, transactions, internal routes - Naming conventions — verbs by layer, type suffixes, booleans, status lexicon, domain vocabulary (legacy terms, French business vocabulary), module names
- Domain map — business glossary, module → bounded-context index, lifecycle & cross-module workflows
- Integration tests — Vitest + real Postgres/Redis, three-layer harness, fixtures, placement
Workflows:
- Review integration tests → review-integration-tests skill (.claude/skills/)
- Audit rules for drift → /rules-hygiene
API non-negotiables¶
These refine the root rules — don't restate them.
- New code goes in
src/__new/modules/on the Kysely + Zod stack. Everything else (TypeORM + idonttrustlikethat,src/db/,src/dtos/) is legacy: never refactor, rename, or "modernize" it unless explicitly asked. - Business errors are values:
Result<T, E>+Err('code' as const)— neverthrow/try-catchfor business flows. - Branded types from
@bricks-common/api-communication*for every id and amount (Cents) — no bare primitives across layers. - Thin controllers: validate → service call →
matchon error codes. Lookups, dispatch, patch building live in services; pure computation inbusiness/(100% unit-tested). - Everything entering the system is validated — zod on the new stack,
httpValidate/queryPgAndValidateon legacy. - Transactions:
safePgTransaction+getAndLock*(FOR UPDATE) for read-modify-write. - Never create tests spontaneously. Offer integration tests (see topical rule); unit tests only for
business/. - Domain docs in the same PR: business-logic or workflow change → update
domain-map.mdcand/or the moduleAGENTS.md/README.md.
Domain language¶
One term per concept — full tables in naming-conventions §6–§7; check them before naming anything. The load-bearing rules:
investor, nevercustomer/user(customer= legacy auth/profile bridge; there is noinvestorstable)project, neverpropertyin new identifiers (properties/property_*/propertyId= frozen legacy names)- French business terms are the canonical identifiers — never translate:
echeancier/echeance/mensualite(notschedule/installment/monthlyPayment),sequestre(notescrow),fiducie,caution,hypotheque - English stays canonical where established:
funding,payout,fee,Mandate,withdraw - French concept names + English status values:
mensualiteStatus: 'paid', never'payee'
API rule-file mapping¶
Per Keep rules in sync with code — change API code, edit the matching rule in the same PR:
- Layer pattern, stack rule, error handling, DO/DO NOT →
./api-conventions.mdc - New verb, suffix, status value, or domain term →
./naming-conventions.mdc - Business subtlety, workflow, glossary entry →
./domain-map.mdc - Integration-test harness, fixtures, placement →
./integration-test-conventions.mdc - API-specific non-negotiable or stack item → this file. Monorepo-wide → root
claude-root.mdc.
What this app is¶
NestJS (Fastify) monolith serving every Bricks surface: investor apps, project-owner financing portal (PDP), back-offices, and internal server-to-server routes. Two isolated domains — investor side and financing side (project-financing-request & friends) — never share tables or entities.
HTTP API plus background workers (graphile-worker cron/queue + dedicated worker processes for P2P, PDF, reservations). Auth is dual-instance better-auth (clientAuth / adminAuth) — see src/lib/better-auth/README.md.
Stack (one-liners)¶
- NestJS 11 on Fastify
- TypeScript:
strict: trueintsconfig.build.json - PostgreSQL (Neon) — per-dev branches, no local DB; Redis remote (cache + queues)
- Data access: Kysely + Zod (new,
ky_*repos), TypeORM + idonttrustlikethat (legacy, frozen) - Migrations: Flyway, forward-only (
migration/flyway/),pnpm migration-file:new <name> - Env: Doppler only (
doppler run -p api -c dev_local -- …) — never.env - Observability: Pino + Datadog tracing
- Tests: Vitest — unit (
*.unit-test.ts) + integration (real Postgres/Redis via docker compose)
Commands¶
From the repo root, pnpm --filter @bricks/api <script>:
| Command | What |
|---|---|
dev |
API server watch mode (wrap with Doppler) |
typecheck |
tsc on tsconfig.build.json + tsconfig.spec.json — never tsc -p tsconfig.json (solution-style, checks nothing) |
build |
nest build |
lint |
Biome + integration-test rules |
test |
Vitest unit tests |
test:integration |
docker compose up → Vitest against real Postgres/Redis (Doppler dev_integration_tests) |
Integration tests exercise the app against the built dist of api-communication* packages — rebuild the package after editing a contract, or the app sees a stale schema.
Project layout¶
projects/api/
├── migration/flyway/ # SQL migrations (forward-only)
├── tests/integration/ # harness, docker, fixtures
└── src/
├── main.ts / app.module.ts
├── __new/
│ ├── lib/ # kysely, validation, providers, worker, …
│ └── modules/<name>/ # new code goes here — one folder per module
│ └── controllers/ services/ business/ repositories/ model/ guards/ lib/
├── db/ # LEGACY TypeORM entities + repositories
├── lib/ # shared libs incl. better-auth
└── <legacy modules>/ # projects/, properties/, customers/, marketplace/, …
Module → bounded-context index: see domain-map — don't duplicate it here.