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.

deckgauge · pnpm lint
eslint typescript-eslint recommended + prettier
prettier single quotes, 2-space, 100 cols

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.tsx file 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, and findByText rather than container.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

SymptomCauseFix
ESLint fails on an unused variable@typescript-eslint/no-unused-vars is an error, not a warningRemove it, or prefix with _ if it's an intentionally-unused parameter
A new schema feels like it belongs in the route fileConvenient, but it can't be reused from apps/webDefine it in packages/shared and import it from both sides
A component test breaks on an unrelated markup tweakIt queried by DOM structure instead of role/textRewrite the assertion against getByRole/getByText

Related

Last updated