Coding Standards & Contributing

Conventions and checks to know before opening a PR.

metrd is in alpha and evolving quickly. Conventions here reflect the current state of the repo, not a fixed spec.

Repository layout

The monorepo has three deployable projects and two shared packages:

ProjectPurpose
API/ASP.NET Core backend - see Tech Stack for the layer breakdown
App/The Next.js user-facing web app
Site/This documentation and marketing site
packages/api-clientGenerated TypeScript client for the API
Vexa/LibraryVexa: the UI framework the App is built on - templates, components, forms, routing and formatting

API conventions

  • Layering: Service → Business → Data, one direction only. Controllers handle input/output, managers hold business logic, repositories are the only thing that touches the database.
  • Entities, not tables: repositories return entity classes, never raw EF table classes, and entities are created through a static Create() factory method.
  • Naming: any Guid property, field, or parameter is suffixed _UID. Global usings live in each project's Usings.cs, not scattered across files or in the .csproj.
  • Style: block-scoped namespaces, explicit types (no var), expression-bodied members where they fit on one line, no primary constructors.
  • Tests: MSTest with Moq. Test classes are named {ClassBeingTested}Test; test methods follow {ClassBeingTested}_{MethodBeingTested}_{ExpectedBehaviour}.
  • Most of this is enforced automatically - a set of custom Roslyn analysers (Metrd.API.Analysers) run as part of the build, covering member ordering, using-directive ordering, one type per file, file/type name matching, and spacing between members, several with automatic code fixes available in-editor. The built-in Roslyn formatter (IDE0055, configured in .editorconfig) covers general spacing.

metrd targets light hardware (see Tech Stack), so treat database round trips as a cost, not a given. Prefer batching over per-item queries, cache reference data that rarely changes, and think about query cost in a PR the same way you'd think about correctness.

App conventions

  • Next.js App Router, TypeScript in strict mode. Dashboard pages are built from Vexa templates, and Vexa's lint rules make every page return one.
  • ESLint runs against .ts/.tsx as part of CI; Prettier formats consistently (2-space indent, double quotes, trailing commas, 120-character lines).
  • Forms use useModelState and Vexa field components for all validation - see Forms & Validation.
  • Every visible string is a translation key, not plain text. Data from the API is shown with plain().
  • Dashboard pages talk to the API on the server, through a *Api.ts file next to the pages. Ask the API for exactly what the page needs rather than filtering in the App.
  • Every page has a url.ts, and links are built from those, so moving a page breaks the build instead of the link.

Before opening a PR

Each project has its own CI check that runs on every pull request:

  • API: builds in Release, checks for pending EF Core migrations that weren't committed, then runs the full test suite with coverage.
  • App: lints, type-checks and builds Vexa, then lints, type-checks, tests and builds the App.
  • Site: installs dependencies, lints, then builds.
  • Docker: builds both images and runs a full Compose smoke test against their health endpoints - see Running with Docker.
  • Version: checks the API and App versions are valid and haven't gone backwards.

Running the equivalent locally before pushing saves a round trip:

# API
cd API
dotnet build Metrd.API.sln
dotnet test Metrd.API.sln
 
# App
cd App
npm run lint
npm run typecheck
npm test
npm run build
 
# Site
cd Site
npm run lint
npm run build

Making changes

  • Keep changes scoped to the layer they belong in - a UI tweak shouldn't touch the API, and a data-model change shouldn't reach into the App.

  • If you change an API controller, hub, or contract used by the client, regenerate it:

    npm run generate-client --workspace=metrd-app
  • If you change the EF Core model, add a migration rather than editing an existing one, and make sure dotnet ef migrations has-pending-model-changes passes - this is exactly what CI checks.

  • To ship a change, bump the version of what it touches: Version in API/Directory.Build.props, or version in App/package.json. If the App now needs something new from the API, raise metrd.minimumApiVersion in App/package.json as well (or MinimumAppVersion for the reverse).