Skip to main content

Coding Standards — Ghasi EHR Platform

Scope: Programming and coding standards enforced system-wide across ALL services and the web application.
Authority: Normative. Every PR/commit MUST comply.


1. Language & Runtime

TargetStandard
LanguageTypeScript — strict: true in every tsconfig.json
BackendNestJS (one process per microservice)
FrontendNext.js 16 (App Router); React Server Components where beneficial
UI libraryMUI v6 + Emotion; RTL-aware theme with design tokens
Node versionLTS (≥ 20)
Package managerpnpm (monorepo workspaces)

2. Design Principles (Refactoring Baseline)

Use these when simplifying deep branching and aligning the codebase.

PrincipleMeaning here
KISSPrefer the smallest change that meets the requirement. Avoid frameworks inside frameworks, speculative abstractions, and “clever” one-liners that hide behavior.
DRYExtract duplication only when the rule is the same — not when code merely looks similar. Wrong abstraction is worse than duplication.
YAGNIDo not add hooks, flags, or extension points “for later.” Implement what is needed now; refactor when a second real use case appears.
Single responsibilityA function does one thing at one level of abstraction. If the name needs “and,” split or extract.
Explicit over implicitPrefer clear names and small helpers over magic values, side effects hidden in getters, or boolean parameters that change behavior (doWork(true, false)).

Refactoring priority: reduce nesting depth and cyclomatic complexity before micro-optimizing. Flat, early-exit control flow is easier to test and review than nested if trees.


3. TypeScript Rules

// tsconfig base — enforced in all packages
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true
}
}

Naming Conventions

ElementConventionExample
Fileskebab-casecreate-allergy.dto.ts
ClassesPascalCaseAllergyService
Interfaces / typesPascalCase, no I prefixAllergyRecord
Functions/methodscamelCasecreateAllergy()
ConstantsUPPER_SNAKE_CASEMAX_RETRY_COUNT
EnumsPascalCase (name), UPPER_SNAKE_CASE (values)AllergyStatus.ACTIVE
DB columnssnake_casetenant_id, created_at
NATS subjectslowercase dot-delimitedclinical.allergies.added
Event typeslowercase dot-delimitedghasi.allergies.added

Type Safety

  • No any — use unknown and narrow with type guards, zod parsing, or assertion functions.
  • Avoid type assertions (as) unless narrowing is provably safe; prefer validation at boundaries (HTTP body, NATS payload, env).
  • Prefer readonly on properties and tuples when values must not change after construction.
  • Use discriminated unions for state machines and variant results ({ status: 'ok'; data: T } | { status: 'error'; code: string }).
  • DTO classes use class-validator decorators; domain types use plain interfaces/types. Do not leak ORM entities across service boundaries.
  • Use satisfies when you want inference plus checked excess properties (e.g. config objects, maps).
  • Use as const for literal unions and frozen configuration objects where appropriate.
  • Prefer undefined over null for “missing” optional values unless an API or library requires null (stay consistent within a module).

Modern TypeScript Patterns

  • Prefer interface for object shapes that may be extended; use type for unions, intersections, mapped types, and utility compositions.
  • Use built-in utility types (Pick, Omit, Partial, Required, Readonly, Record, NonNullable) and small named type aliases instead of repeating large inline shapes.
  • enum: prefer string unions or as const objects for tree-shaking and simpler JSON; if enum is used, keep values UPPER_SNAKE_CASE per table above.
  • Model impossible states as unrepresentable (e.g. don’t allow endDate without startDate at the type level when feasible).
  • async functions MUST await or return the Promise; do not leave floating promises. Use void prefix only for intentional fire-and-forget with a comment explaining why.

Control Flow & Readability (Nested if / Sub-if)

Deep nesting is a code smell. Prefer the following, in order of applicability:

  1. Guard clauses / early return — handle invalid or terminal cases first; keep the “happy path” last at low indentation.

    // Prefer
    if (!entity) {
    throw new NotFoundException('…');
    }
    if (entity.tenantId !== tenantId) {
    throw new ForbiddenException('…');
    }
    return mapToDto(entity);

    // Avoid
    if (entity) {
    if (entity.tenantId === tenantId) {
    return mapToDto(entity);
    } else {
    throw new ForbiddenException('…');
    }
    } else {
    throw new NotFoundException('…');
    }
  2. Extract predicates — name conditions: isEligibleForRefund(order), canTransitionTo(status), not a five-clause if inline.

  3. Lookup tables / maps — replace long if-else or switch chains that only map keys to values or handlers (use Record, Map, or a const object).

  4. switch with exhaustive checks — for discriminated unions, use switch (x.kind) and assertNever(x) in default during development to catch missing cases.

  5. Separate policy from mechanics — validation, authorization, and I/O should not be interleaved in one giant block; call small functions in sequence.

Avoid:

  • Boolean parameters that switch major behavior — use two functions or an options object with a discriminant.
  • Deeply nested ternaries — use if or a well-named helper.
  • “Flag soup” (if (a && b && !c || d)) — extract to a named boolean or a short comment-worthy helper.

Functions & Modules

  • Prefer pure functions where possible; isolate side effects (DB, HTTP, messaging) in thin layers.
  • Default export: avoid in shared library code; prefer named exports for consistent renaming and refactors.
  • Barrel files (index.ts): use sparingly in large trees; they can hurt static analysis and create circular imports. Prefer direct imports for hot paths or cycle-prone areas.
  • File length: if a file exceeds ~300–400 lines and mixes unrelated concerns, split by feature or layer (dto / service / repository), not by arbitrary line count alone.

4. NestJS Conventions

Module Organization

  • One NestJS module per bounded context feature area.
  • Guard registration: APP_GUARD for global guards (JWT, tenant); per-controller for domain guards.
  • Use @Permissions() on protected controller methods (see ADR specs/architecture/adr/0001-permission-based-authorization.md). Register PermissionsGuard where HTTP authorization is required; a guard without required permission metadata on the handler is a critical bug for protected routes.

Dependency Injection

  • Inject via constructor; prefer interfaces with @Inject() token for testability.
  • Services MUST NOT call other service constructors directly — use DI.

Controller Rules

  • Controllers handle HTTP concerns only: parse request, call service, return response.
  • No business logic in controllers.
  • Use @ApiTags(), @ApiOperation(), @ApiResponse() for OpenAPI documentation.

5. Database & ORM

  • TypeORM with migration-based schema management.
  • Entity files in entities/ directory.
  • Repository pattern: use TypeORM repositories or custom repository classes.
  • Never use synchronize: true in non-test environments.
  • Transactions: use QueryRunner or EntityManager.transaction() for multi-table writes.
  • Optimistic locking via version column.

6. Event Publishing

  • Use @ghasi/nats-client CloudEventsBuilder for all events.
  • Validate event payloads against @ghasi/event-schemas Zod schemas.
  • Publish AFTER successful persistence.
  • Event data payloads carry minimal identifiers (not full entities).

7. Error Handling

  • Use NestJS exception filters for consistent error responses.
  • Throw typed exceptions: BadRequestException, ForbiddenException, etc.
  • Include correlation ID in every error response.
  • Log errors with structured fields: { correlationId, tenantId, service, error }.
  • Do not swallow errors — log or rethrow with context; empty catch blocks are forbidden except with a documented, reviewed reason.

8. Shared Packages

Monorepo shared packages under packages/@ghasi/:

PackagePurpose
shared-typesCommon DTOs, error codes, type definitions
event-schemasZod schemas for CloudEvents data payloads
nats-clientNATS JetStream helpers, CloudEventsBuilder, DLQ patterns
auth-guardJWT validation guard, tenant extraction, role decorators
access-clientABAC evaluation client for Access Policy Service
audit-clientAudit event publishing helpers
test-utilsTest factories, fixtures, mocking helpers

8.1 Next.js and BFF (apps/ehr-web)

§8 lists platform @ghasi/* libraries (events, Nest auth, server-side access client, audit). It does not replace rules for how the Next.js app talks to Route Handlers and the gateway. The following applies to apps/ehr-web (and the same pattern SHOULD be followed for other Next apps with a BFF layer).

RuleRequirement
BFF boundaryMUST use same-origin Next.js Route Handlers (/api/...) as the default path from browser code to backend capabilities. MUST NOT call Kong or microservice origins from browser code except where an existing, documented exception applies (default: Route Handler → gateway).
fetchUsing the standard fetch API to invoke /api/... is permitted and is the normal mechanism for Client Components and hooks.
Client modulesSHOULD consolidate domain calls in apps/ehr-web/src/clients/ (e.g. clients/clinical/)—typed helpers, shared auth (serviceToken / session), and stable URLs—instead of repeating ad-hoc fetch('/api/...') across views. New surface area SHOULD add or extend named functions in the appropriate client module (or a colocated *Api.ts).
UI access controlFor permission-aware UI in the browser, SHOULD use @ghasi/access-client-browser (AccessProvider, useAccess, AccessGuard, fetchResolvedAccessProfile) with profiles resolved via the app’s access BFF—not ad-hoc permission strings with no shared contract.

Normative companion (worked examples and agent defaults): .cursor/rules/ehr-web-bff-api-clients.md (repository root).


9. Git & Code Quality

  • Commits: Conventional Commits format (feat:, fix:, chore:, docs:, test:, refactor:)
  • Branches: feature/{module}-{description}, fix/{module}-{description}
  • Linting: ESLint with @typescript-eslint — zero warnings policy
  • Formatting: Prettier — enforced via pre-commit hook
  • No console.log in production code — use structured logger

10. Import Order

// 1. Node built-ins
import { readFile } from 'fs/promises';

// 2. External packages
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

// 3. Monorepo shared packages
import { CloudEventsBuilder } from '@ghasi/nats-client';

// 4. Local imports (relative)
import { CreateAllergyDto } from './dto/create-allergy.dto';
import { AllergyEntity } from './entities/allergy.entity';

11. Documentation

  • Every service MUST have an OpenAPI spec (auto-generated via NestJS Swagger decorators).
  • Document the contract, not the obvious: every exported public API (services, libraries, non-trivial utilities) MUST have JSDoc/TSDoc describing parameters, return value, thrown errors, and invariants when non-obvious. Private methods: document when behavior is non-obvious or security-sensitive.
  • README.md per service with: purpose, setup, environment variables, API overview.

12. Testing Expectations (Alignment)

  • Unit tests for pure logic, mappers, guards, and complex conditionals extracted from controllers.
  • When refactoring nested if trees into helpers, add or update tests that pin behavior before structural changes (characterization tests where legacy behavior must be preserved).
  • Target ≥ 80% coverage on new or heavily refactored code per platform policy; focus on branches and edge cases that were previously buried in nesting.