9.2 KiB
9.2 KiB
Coding Conventions & Style Guide — MeshiTrack
Cross-cutting conventions that apply to all packages in the monorepo.
General Rules
No emojis
Never use emoji characters anywhere in this codebase: not in source code, UI text, console output, log messages, comments, or documentation. Use plain text instead.
Language: TypeScript everywhere
- All packages use TypeScript with
strict: true - No
.jsfiles in source (except config files:jest.config.js,next.config.js) - All files use
.tsor.tsxextension
Formatting: Prettier
// .prettierrc
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"arrowParens": "always",
"endOfLine": "lf"
}
Linting: ESLint
Use a flat config (eslint.config.js) with TypeScript support:
@typescript-eslint/recommended@typescript-eslint/no-explicit-any→ error@typescript-eslint/no-unused-vars→ error (with_prefix exception)import/order→ enforce consistent import ordering
Import ordering
// 1. Node built-ins
import { readFile } from 'fs/promises';
// 2. External packages
import { Injectable } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
// 3. Monorepo packages
import type { Product } from '@meshitrack/shared';
import { ProductCategory } from '@meshitrack/shared';
// 4. Internal (relative) imports
import { ProductsRepository } from './products.repository';
import type { ProductQueryDto } from './dto/product-query.dto';
Separate each group with a blank line.
Naming Conventions
Files
| Kind | Pattern | Example |
|---|---|---|
| Module | kebab-case.module.ts |
products.module.ts |
| Controller | kebab-case.controller.ts |
products.controller.ts |
| Service | kebab-case.service.ts |
products.service.ts |
| Repository | kebab-case.repository.ts |
products.repository.ts |
| Schema (Mongoose) | kebab-case.schema.ts |
product.schema.ts |
| DTO | kebab-case.dto.ts |
create-product.dto.ts |
| Guard | kebab-case.guard.ts |
keycloak-auth.guard.ts |
| Decorator | kebab-case.decorator.ts |
current-user.decorator.ts |
| Interface | kebab-case.interface.ts |
llm-provider.interface.ts |
| React component | PascalCase.tsx |
ProductCard.tsx |
| React hook | use-kebab-case.ts |
use-products.ts |
| Test file | *.spec.ts (API) / *.test.tsx (Web) |
products.service.spec.ts |
| Zod schema | kebab-case.schemas.ts |
product.schemas.ts |
| Enum file | kebab-case.enums.ts |
product.enums.ts |
Code
| Kind | Style | Example |
|---|---|---|
| Class | PascalCase | ProductsService, KeycloakAuthGuard |
| Interface | PascalCase + I prefix (optional) | ILlmProvider or LlmProvider |
| Type alias | PascalCase | ProductQuery, CreateProductInput |
| Enum | PascalCase | ProductCategory |
| Enum member | UPPER_SNAKE | ProductCategory.DAIRY |
| Function | camelCase | calculateNutrition() |
| Variable | camelCase | servingSize, totalCalories |
| Constant | UPPER_SNAKE | LLM_PROVIDER, MAX_RETRY_COUNT |
| Private field (backing getter/setter) | _camelCase |
_accessToken, _householdId |
| React component | PascalCase | ProductCard, NutritionBadge |
| React hook | camelCase | useProducts, usePantryItems |
| CSS class (Tailwind) | kebab-case | via Tailwind utilities |
Code Organization Rules
API (NestJS)
- One module per feature — self-contained folder with controller, service, repository, schemas, DTOs, tests
- No cross-module imports of internal files — only import from exported module API
- Services contain business logic — controllers are thin, repositories handle data access
- Throw NestJS exceptions —
NotFoundException,ConflictException, etc. - Use custom decorators for request context —
@CurrentUser(),@CurrentHousehold()
Web (Next.js)
- App Router conventions —
page.tsx,layout.tsx,loading.tsx,error.tsx - Server Components by default — only add
'use client'when interactive - Feature components in
components/features/— organized by domain (products, pantry, etc.) - UI components in
components/ui/— generic, reusable, no domain logic - Hooks in
hooks/— custom hooks for data fetching, state management - Services in
services/— API client wrappers
Shared
- Only pure TypeScript — no Node.js, no browser, no framework code
- Types, enums, Zod schemas, pure utils — nothing else
- Barrel exports — every directory has
index.ts - Use
typemodifier for type-only exports/imports
Getters and Setters
Use TypeScript get/set accessors when a property simply exposes or lightly wraps a private backing field. Use a plain method only when the operation is async, takes multiple parameters, or has meaningful side effects beyond assignment.
// Good:Simple exposure of a private field — use accessor
class ApiClient {
private _accessToken: string | null = null;
set accessToken(token: string) {
this._accessToken = token;
}
get accessToken(): string | null {
return this._accessToken;
}
}
// Good:Side effects / async / multiple params — use a method
class TokenManager {
async setTokenFromCode(code: string, redirectUri: string) {
const token = await exchangeCode(code, redirectUri);
this._accessToken = token;
}
}
Backing fields use the _camelCase prefix to avoid name collisions with the accessor.
Error Messages
API error response format
{
"statusCode": 404,
"error": "Not Found",
"message": "Product abc123 not found in household xyz",
"timestamp": "2026-01-15T10:30:00.000Z",
"path": "/api/v1/products/abc123"
}
Rules
- Messages are end-user readable — no stack traces, no internal IDs in production
- Include resource type and identifier in not-found messages
- Log full error details server-side with a correlation ID
- Never leak database field names or query details
Git Conventions
Branch naming
feature/MESH-001-product-crud
fix/MESH-042-barcode-duplicate
chore/update-dependencies
docs/phase-2-recipe-notes
Commit messages (Conventional Commits)
feat(products): add barcode lookup via Open Food Facts
fix(pantry): correct freshness calculation for opened items
docs: update phase-2 recipe instructions
chore(deps): bump @nestjs/core to 10.4.0
test(api): add integration tests for product search
refactor(api): extract repository pattern from service
PR template
## What
Brief description of the change.
## Why
Link to issue or explanation of the need.
## How
Key implementation decisions.
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests (if DB changes)
- [ ] Manual testing done
## Checklist
- [ ] Types updated in `packages/shared`
- [ ] Zod schemas match new fields
- [ ] API docs (Swagger decorators) updated
- [ ] No `any` types introduced
Logging
Use NestJS Logger
import { Logger } from '@nestjs/common';
@Injectable()
export class ProductsService {
private readonly logger = new Logger(ProductsService.name);
async create(householdId: string, userId: string, dto: CreateProductDto) {
this.logger.log(`Creating product "${dto.name}" for household ${householdId}`);
// ...
this.logger.debug(`Product created with ID ${product.id}`);
return product;
}
}
Log levels
error— unexpected failures, unhandled exceptionswarn— recoverable issues, deprecated usage, slow querieslog— significant operations (create, delete, status transitions)debug— detailed flow information (only shown in dev)verbose— very detailed (disabled by default)
Never log:
- Passwords, tokens, API keys
- Full request bodies with sensitive user data
- PII (unless specifically needed and compliant with privacy policy)