MeshiTrack/docs/style-guidelines.md

6.2 KiB

MeshiTrack Coding Style & Engineering Guidelines

This document establishes the official coding standards, architectural paradigms, and TypeScript guidelines for the MeshiTrack monorepo. These guidelines are designed to enforce clean, testable, object-oriented codebases with pristine type-safety under strict quality gates.


🏛️ 1. Encapsulation & Class Member Accessibility

To ensure clear visibility boundaries, clean interfaces, and easy maintainability:

  1. Mandatory Accessibility Modifiers: Every field, constructor, and method in a class must explicitly declare its accessibility modifier (public, protected, or private).
  2. Mandate Explicit Fields: Classes that participate in dependency injection (e.g. Services, Repositories, Providers) must explicitly declare their properties at the class level rather than relying on TypeScript constructor parameter properties.
  3. Class Member Ordering: Group class members in a predictable sequence:
    • Static Fields (Public -> Protected -> Private)
    • Instance Fields (Public -> Protected -> Private)
    • Constructor
    • Public Methods
    • Protected Methods
    • Private Methods

Example:

export class ShoppingListsService {
  // 1. Explicit, readonly instance properties
  private readonly shoppingListsRepository: ShoppingListsRepository;

  // 2. Explicit constructor receiving DI payload
  public constructor({ shoppingListsRepository }: Deps) {
    this.shoppingListsRepository = shoppingListsRepository;
  }

  // 3. Public class methods
  public async list(householdId: string): Promise<ShoppingListDocument[]> {
    return this.shoppingListsRepository.list(householdId);
  }
}

🛡️ 2. Boundary Types & Return Types

To prevent type-drift across software boundaries:

  1. Explicit Return Types on Boundary Functions: All public methods of class instances (Services, Repositories, Providers) and all exported module functions must explicitly declare their return types.
  2. Relegate Inference to Callbacks: Local, inline variables and anonymous arrow callbacks (e.g. within array map/filter functions) may utilize automatic type inference for conciseness.

Example:

// 🟢 Good: Explicitly defined return type on the boundary method
public async list(householdId: string): Promise<ShoppingList[]> {
  const lists = await this.shoppingListsRepository.list(householdId);
  return lists.filter(item => item.status === 'active'); // Inline inference is okay
}

🚫 3. Eradicating any & Enforcing Type-Safety

TypeScript's any type compromises the integrity of our codebase and makes refactoring fragile.

  1. No Explicit any: The use of explicit any is strictly prohibited throughout the codebase, including in test files.
  2. Restrict Unsafe Operations: We enforce strict type-checked rules to prevent the propagation of any:
    • Assigning an any to a variable/property is blocked (@typescript-eslint/no-unsafe-assignment).
    • Reading a property off of an any is blocked (@typescript-eslint/no-unsafe-member-access).
    • Invoking functions typed as any is blocked (@typescript-eslint/no-unsafe-call).
    • Returning an any from a function is blocked (@typescript-eslint/no-unsafe-return).
  3. Prefer unknown for Dynamic Inputs: Any unchecked external input (e.g., API payloads, database responses, dynamic JSON files) must be typed as unknown and validated at the boundary using Zod schemas or type guards.
  4. Type-Safe Mocking: Instead of casting mocks with as any, use highly targeted partial types or precise mocks.

🏷️ 4. Naming Conventions & File Structures

Uniform naming conventions prevent conflicts and reduce cognitive load:

  1. Directories: Lowercase kebab-case (e.g., shopping-lists/).
  2. Files: PascalCase matching the primary class or component exported in the file (e.g., ShoppingListsService.ts), ensuring class name and file name are aligned.
  3. Identifiers:
    • PascalCase: Classes, Interfaces, Type Aliases, Enums.
    • camelCase: Properties, Methods, Variables, Parameters, Functions.
    • UPPER_CASE: Top-level static read-only constants.
  4. Prohibit Hungarian Notation: Do not prefix interfaces with I (e.g. write ShoppingListsRepository, NOT IShoppingListsRepository).

🛑 5. Structured Domain Exceptions

Exception mapping must be uniform, semantic, and highly testable:

  1. Prohibit Generic Error Throwing: The service and business layers must never throw generic new Error('message') for operational domain failures.
  2. Domain-Specific Exceptions: Throw subclasses of AppError (e.g. NotFoundError, ConflictError, BadRequestError) containing semantic properties like status codes and optional structured sub-details.
  3. Prevent Stack Leakage: Our custom exceptions map directly to HTTP responses in central middleware, ensuring internal database schemas or runtime details are never leaked to external clients.

Example:

// 🟢 Good: Highly testable, structured domain-level exception
if (!updated) {
  throw new NotFoundError('Shopping list not found');
}

🧱 6. OOP vs. Functional Boundaries

We keep class-based OOP strictly segregated from pure stateless helpers:

  1. Stateful Layers -> Classes: Services, Repositories, and Providers must be class-based to properly encapsulate state, DI, and persistence hooks.
  2. Stateless Operations -> Standalone Pure Functions: Math utilities, date/string formatters, and mapping helpers must be written as standalone, exported functions in pure modules.
  3. No Utility Classes: Do not write static utility classes (classes composed solely of static helper methods). Use direct ES module exports.

7. Asynchronous Programming Integrity

To prevent background crashes, memory leaks, and silent failures:

  1. Mandate async/await: Use async/await exclusively instead of Promise chains (.then(), .catch()).
  2. Block Floating Promises: Every Promise must be awaited or returned.
  3. Explicit Voiding for Intentional Background Tasks: If a task must run in the background (fire-and-forget), it must be explicitly voided (e.g. void this.trackEvent()) and handle its exceptions internally to prevent uncaught runtime rejections.