3.9 KiB
3.9 KiB
Code Standards
The living rulebook for this codebase. One canonical way to do each thing, enforced in CI so it can't drift. Sections marked (enforced) fail CI; (target) are being rolled out. Update this doc when a standard changes.
Tooling & enforcement (enforced)
- Formatting: Prettier (
.prettierrc.json) — single quotes, semicolons, 2-space,printWidth: 100, trailing commas. Runnpm run format; CI runsformat:check. - Lint:
npm run lint(eslint .) covers client and server. Focused rule set (react-hooks on the client;js/recommended+ TS-unused on the server). Errors block CI; warnings are advisory (ratchet down over time). - Typecheck:
npm run typecheck(client, strict +noUncheckedIndexedAccess) andnpm run typecheck:server(server). Both run in CI. - Local hooks (lefthook): pre-commit auto-formats staged files + lints; pre-push
typechecks. Installed by
npm install(thepreparescript). - Dead code:
npm run knip(advisory) for unused exports/deps. - Full gate:
npm run ci= format:check + lint + typecheck (×2) + check:server + tests (server + client) + build. The CI pipeline runs the same.
Modules & files
- Server is CommonJS TypeScript
.cts, run by Node via type-stripping (no build step). Every migrated module isrequired with the explicit.cts/.mtsextension (extensionless does not resolve). Each.ctsleads with animport type …line so type-stripping activates. - Client is ESM
.ts/.tsxwith the@/*path alias. - Money is integer cents in the DB and on the wire; convert to dollars only at the edge
via the money helpers. Branded
Cents/Dollarstypes where practical.
HTTP API conventions
- Wire format: JSON, snake_case field names (mirrors DB columns), money as integer cents.
- Error responses (target): every error body is
{ error, message, code, field? }with the correct HTTP status. Throw the factories fromutils/apiError.cts(ValidationError,NotFoundError,ConflictError, …) and let the single terminal error handler format them — do not hand-rollres.status().json({ error }). Express 5 forwards rejected async handlers automatically, so route bodies should not wrap everything intry/catch. Clients readmessage/code+ status. - Success envelope (target): reads return the bare resource/list; mutations return
{ success: true, … }. - Validation (target): validate inputs with the shared validators (throwing
ValidationError); do not scatter ad-hocparseInt/range checks.
Security (target; see also SECURITY.md)
- Every mutating route carries CSRF + auth + an appropriate rate-limit + input
validation. Admin routes require
requireAdmin. - Every user-scoped query filters
user_idanddeleted_at IS NULL. Never interpolate user input into SQL — use bound parameters. - Never log secrets, tokens, or raw financial values.
- Fail fast at boot on missing required config (e.g.
TOKEN_ENCRYPTION_KEYin production).
Logging (target)
- One
loghelper with levels; env-driven level; redaction of secrets/PII. Replace rawconsole.*over time.
Data integrity
- Every multi-statement write runs inside
db.transaction(). - Migrations are append-only versioned entries in
db/migrations/versionedMigrations.cts(idempotent; guard column adds withPRAGMA table_info).
Testing
- Server:
node:testintests/*.test.js. Client: Vitest (*.test.ts[x]). E2E: Playwright (npm run test:e2e, probe subsettest:e2e:probe). Add coverage for new handlers; prove money invariants with property tests where practical.
Future / modernization (tracked, not yet adopted)
Kysely (typed SQL) · Zod (validation) · pino (logging) · native --env-file + validated
config · node:sqlite · ts-rest + OpenAPI · Vite 6/7. Adopt where each makes the code more
uniform; see the standardization plan.