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
| Target | Standard |
|---|---|
| Language | TypeScript — strict: true in every tsconfig.json |
| Backend | NestJS (one process per microservice) |
| Frontend | Next.js 16 (App Router); React Server Components where beneficial |
| UI library | MUI v6 + Emotion; RTL-aware theme with design tokens |
| Node version | LTS (≥ 20) |
| Package manager | pnpm (monorepo workspaces) |
2. Design Principles (Refactoring Baseline)
Use these when simplifying deep branching and aligning the codebase.
| Principle | Meaning here |
|---|---|
| KISS | Prefer the smallest change that meets the requirement. Avoid frameworks inside frameworks, speculative abstractions, and “clever” one-liners that hide behavior. |
| DRY | Extract duplication only when the rule is the same — not when code merely looks similar. Wrong abstraction is worse than duplication. |
| YAGNI | Do not add hooks, flags, or extension points “for later.” Implement what is needed now; refactor when a second real use case appears. |
| Single responsibility | A function does one thing at one level of abstraction. If the name needs “and,” split or extract. |
| Explicit over implicit | Prefer 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
| Element | Convention | Example |
|---|---|---|
| Files | kebab-case | create-allergy.dto.ts |
| Classes | PascalCase | AllergyService |
| Interfaces / types | PascalCase, no I prefix | AllergyRecord |
| Functions/methods | camelCase | createAllergy() |
| Constants | UPPER_SNAKE_CASE | MAX_RETRY_COUNT |
| Enums | PascalCase (name), UPPER_SNAKE_CASE (values) | AllergyStatus.ACTIVE |
| DB columns | snake_case | tenant_id, created_at |
| NATS subjects | lowercase dot-delimited | clinical.allergies.added |
| Event types | lowercase dot-delimited | ghasi.allergies.added |
Type Safety
- No
any— useunknownand narrow with type guards,zodparsing, or assertion functions. - Avoid type assertions (
as) unless narrowing is provably safe; prefer validation at boundaries (HTTP body, NATS payload, env). - Prefer
readonlyon 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-validatordecorators; domain types use plain interfaces/types. Do not leak ORM entities across service boundaries. - Use
satisfieswhen you want inference plus checked excess properties (e.g. config objects, maps). - Use
as constfor literal unions and frozen configuration objects where appropriate. - Prefer
undefinedovernullfor “missing” optional values unless an API or library requiresnull(stay consistent within a module).
Modern TypeScript Patterns
- Prefer
interfacefor object shapes that may be extended; usetypefor 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 oras constobjects for tree-shaking and simpler JSON; ifenumis used, keep values UPPER_SNAKE_CASE per table above.- Model impossible states as unrepresentable (e.g. don’t allow
endDatewithoutstartDateat the type level when feasible). asyncfunctions MUSTawaitor return the Promise; do not leave floating promises. Usevoidprefix 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:
-
Guard clauses / early return — handle invalid or terminal cases first; keep the “happy path” last at low indentation.
// Preferif (!entity) {throw new NotFoundException('…');}if (entity.tenantId !== tenantId) {throw new ForbiddenException('…');}return mapToDto(entity);// Avoidif (entity) {if (entity.tenantId === tenantId) {return mapToDto(entity);} else {throw new ForbiddenException('…');}} else {throw new NotFoundException('…');} -
Extract predicates — name conditions:
isEligibleForRefund(order),canTransitionTo(status), not a five-clauseifinline. -
Lookup tables / maps — replace long
if-elseorswitchchains that only map keys to values or handlers (useRecord,Map, or a const object). -
switchwith exhaustive checks — for discriminated unions, useswitch (x.kind)andassertNever(x)indefaultduring development to catch missing cases. -
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
ifor 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_GUARDfor global guards (JWT, tenant); per-controller for domain guards. - Use
@Permissions()on protected controller methods (see ADRspecs/architecture/adr/0001-permission-based-authorization.md). RegisterPermissionsGuardwhere 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: truein non-test environments. - Transactions: use
QueryRunnerorEntityManager.transaction()for multi-table writes. - Optimistic locking via
versioncolumn.
6. Event Publishing
- Use
@ghasi/nats-clientCloudEventsBuilderfor all events. - Validate event payloads against
@ghasi/event-schemasZod 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
catchblocks are forbidden except with a documented, reviewed reason.
8. Shared Packages
Monorepo shared packages under packages/@ghasi/:
| Package | Purpose |
|---|---|
shared-types | Common DTOs, error codes, type definitions |
event-schemas | Zod schemas for CloudEvents data payloads |
nats-client | NATS JetStream helpers, CloudEventsBuilder, DLQ patterns |
auth-guard | JWT validation guard, tenant extraction, role decorators |
access-client | ABAC evaluation client for Access Policy Service |
audit-client | Audit event publishing helpers |
test-utils | Test 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).
| Rule | Requirement |
|---|---|
| BFF boundary | MUST 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). |
fetch | Using the standard fetch API to invoke /api/... is permitted and is the normal mechanism for Client Components and hooks. |
| Client modules | SHOULD 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 control | For 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.login 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
iftrees 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.