Aller au contenu

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.md and everything under projects/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, Result pattern, 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) — never throw/try-catch for 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 → match on error codes. Lookups, dispatch, patch building live in services; pure computation in business/ (100% unit-tested).
  • Everything entering the system is validated — zod on the new stack, httpValidate/queryPgAndValidate on 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.mdc and/or the module AGENTS.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, never customer/user (customer = legacy auth/profile bridge; there is no investors table)
  • project, never property in new identifiers (properties/property_*/propertyId = frozen legacy names)
  • French business terms are the canonical identifiers — never translate: echeancier/echeance/mensualite (not schedule/installment/monthlyPayment), sequestre (not escrow), 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: true in tsconfig.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.jsonnever 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.