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:
- Mandatory Accessibility Modifiers: Every field, constructor, and method in a class must explicitly declare its accessibility modifier (
public,protected, orprivate). - 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.
- 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:
- 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.
- 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.
- No Explicit
any: The use of explicitanyis strictly prohibited throughout the codebase, including in test files. - Restrict Unsafe Operations: We enforce strict type-checked rules to prevent the propagation of
any:- Assigning an
anyto a variable/property is blocked (@typescript-eslint/no-unsafe-assignment). - Reading a property off of an
anyis blocked (@typescript-eslint/no-unsafe-member-access). - Invoking functions typed as
anyis blocked (@typescript-eslint/no-unsafe-call). - Returning an
anyfrom a function is blocked (@typescript-eslint/no-unsafe-return).
- Assigning an
- Prefer
unknownfor Dynamic Inputs: Any unchecked external input (e.g., API payloads, database responses, dynamic JSON files) must be typed asunknownand validated at the boundary using Zod schemas or type guards. - 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:
- Directories: Lowercase
kebab-case(e.g.,shopping-lists/). - Files:
PascalCasematching the primary class or component exported in the file (e.g.,ShoppingListsService.ts), ensuring class name and file name are aligned. - Identifiers:
- PascalCase: Classes, Interfaces, Type Aliases, Enums.
- camelCase: Properties, Methods, Variables, Parameters, Functions.
- UPPER_CASE: Top-level static read-only constants.
- Prohibit Hungarian Notation: Do not prefix interfaces with
I(e.g. writeShoppingListsRepository, NOTIShoppingListsRepository).
🛑 5. Structured Domain Exceptions
Exception mapping must be uniform, semantic, and highly testable:
- Prohibit Generic
ErrorThrowing: The service and business layers must never throw genericnew Error('message')for operational domain failures. - Domain-Specific Exceptions: Throw subclasses of
AppError(e.g.NotFoundError,ConflictError,BadRequestError) containing semantic properties like status codes and optional structured sub-details. - 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:
- Stateful Layers -> Classes: Services, Repositories, and Providers must be class-based to properly encapsulate state, DI, and persistence hooks.
- Stateless Operations -> Standalone Pure Functions: Math utilities, date/string formatters, and mapping helpers must be written as standalone, exported functions in pure modules.
- 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:
- Mandate
async/await: Useasync/awaitexclusively instead of Promise chains (.then(),.catch()). - Block Floating Promises: Every Promise must be
awaited or returned. - 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.