Code conventions
These aren't style preferences — they're the patterns the codebase already follows, and a PR that breaks them stands out in review. Each one below points at where it's stated or demonstrated.
What a clean run reports — ESLint and Prettier both configured to fail loudly, not silently reformat.
The service/routes split — apps/api
Every domain under apps/api/src is one *.service.ts (business logic and Prisma queries) plus one *.routes.ts (the Fastify plugin: HTTP methods, params, and validation) — stated directly in the project's CLAUDE.md (an internal doc, not part of the public OSS snapshot — the convention itself still holds either way, as the code below shows). apps/api/src/boards/board.routes.ts instantiates a BoardService and calls its methods; the service never touches request/reply, and the route never runs a Prisma query directly. Routes are registered as Fastify plugins in apps/api/src/server.ts — see how boardRoutes, widgetDataRoutes, and every other domain get protectedApp.register(...)'d there behind the Keycloak auth plugin.
Request/response shapes live in packages/shared
The intent, per CLAUDE.md, is that every request and response shape is a Zod schema in packages/shared so both apps/web and apps/api validate against the same definition. packages/shared/src/widget-data-batch-schemas.ts is a clean example: it exports WidgetDataBatchRequestSchema, and apps/api/src/widgets/widget-data.routes.ts imports it straight from @deckgauge/shared to validate the batched widget-data request with .safeParse(). Not every older domain fully follows this yet — apps/api/src/boards/board.service.ts still defines CreateBoardInputSchema locally rather than in packages/shared — so when you're touching a domain, put a new shape in packages/shared even if the file next to you doesn't.
packages/db is the source of DB types
packages/db/src/index.ts re-exports the generated Prisma client and its model types (PrismaClient, Board, User, and so on) alongside the shared ClickHouse client. Import model types from @deckgauge/db, not from @prisma/client directly — that's what keeps a schema change in packages/db/prisma/schema.prisma a one-package update instead of a repo-wide find-and-replace.
Tests: co-located, behaviour-focused, TDD for business logic
CLAUDE.md's testing section is explicit on three points, and the codebase backs each one up:
- Co-located — a
*.test.ts/*.test.tsxfile sits next to the source file it tests, the same pattern throughout services, builders, and components. Note that the published open-source snapshot of this repo strips every test file (*.test.ts(x),__tests__,e2e,fixtures) before publishing — so if you're contributing against the public repo, there's no existing test alongside a file to copy from. Write your new file's test the same way the pattern implies: next to the source, before or alongside the implementation. - Behaviour-focused, semantic queries — component tests use
screen.getByRole,getByText, andfindByTextrather thancontainer.querySelector, so a test still passes if the markup changes but the behaviour doesn't. For a new widget component, that means asserting on what a user would actually see — a loading state, an empty-source message, and the rendered value (including any tier or badge) for each state it can be in — not on DOM structure. - TDD for business logic — validators, services, and data transforms are expected to be written test-first. For a new ClickHouse query builder, that means asserting on the SQL it produces for each source-scope combination before trusting its output anywhere else.
If it looks wrong
| Symptom | Cause | Fix |
|---|---|---|
| ESLint fails on an unused variable | @typescript-eslint/no-unused-vars is an error, not a warning | Remove it, or prefix with _ if it's an intentionally-unused parameter |
| A new schema feels like it belongs in the route file | Convenient, but it can't be reused from apps/web | Define it in packages/shared and import it from both sides |
| A component test breaks on an unrelated markup tweak | It queried by DOM structure instead of role/text | Rewrite the assertion against getByRole/getByText |
Related
- Architecture — where each domain and package sits.
- Adding an intelligence widget — these conventions applied to one real feature.
- Setting up to develop — the lint/build/test gates a PR runs through.
Last updated