BillTracker/docs/CODE_STANDARDS.md

3.9 KiB
Raw Blame History

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. Run npm run format; CI runs format: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) and npm 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 (the prepare script).
  • 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 is required with the explicit .cts/.mts extension (extensionless does not resolve). Each .cts leads with an import type … line so type-stripping activates.
  • Client is ESM .ts/.tsx with 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/Dollars types 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 from utils/apiError.cts (ValidationError, NotFoundError, ConflictError, …) and let the single terminal error handler format them — do not hand-roll res.status().json({ error }). Express 5 forwards rejected async handlers automatically, so route bodies should not wrap everything in try/catch. Clients read message/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-hoc parseInt/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_id and deleted_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_KEY in production).

Logging (target)

  • One log helper with levels; env-driven level; redaction of secrets/PII. Replace raw console.* 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 with PRAGMA table_info).

Testing

  • Server: node:test in tests/*.test.js. Client: Vitest (*.test.ts[x]). E2E: Playwright (npm run test:e2e, probe subset test: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.