272 lines
10 KiB
Markdown
272 lines
10 KiB
Markdown
# 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 `.js` files in source (except config files: `jest.config.js`, `next.config.js`)
|
|
- All files use `.ts` or `.tsx` extension
|
|
|
|
### Formatting: Prettier
|
|
|
|
```json
|
|
// .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
|
|
|
|
```typescript
|
|
// 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 |
|
|
|
|
## Implementation Workflow (Vertical Slice)
|
|
|
|
Every feature or phase implementation must follow this order to ensure consistency and type safety across the monorepo:
|
|
|
|
1. **Shared Layer (`packages/shared`)**: Define types, enums, and Zod schemas. Add unit tests.
|
|
2. **Database Layer (`packages/api`)**: Create Mongoose schema and Repository. Use `.lean().exec()` on all reads. Add integration tests.
|
|
3. **Service Layer (`packages/api`)**: Implement business logic using Awilix DI. Add unit tests.
|
|
4. **Route Layer (`packages/api`)**: Create Fastify route plugin. Add route tests.
|
|
5. **Web API Client (`packages/web`)**: Implement frontend service. Add unit tests.
|
|
6. **Web UI (`packages/web`)**: Create Next.js pages/components (Server Components by default). Add component tests.
|
|
|
|
**Mandatory Verification**: Every task must end with `npm run build`, `npm run test:cov`, and `npm run lint` all passing with the defined coverage thresholds.
|
|
|
|
## Code Organization Rules
|
|
|
|
### API (NestJS)
|
|
|
|
1. **One module per feature** — self-contained folder with controller, service, repository, schemas, DTOs, tests
|
|
2. **No cross-module imports of internal files** — only import from exported module API
|
|
3. **Services contain business logic** — controllers are thin, repositories handle data access
|
|
4. **Throw NestJS exceptions** — `NotFoundException`, `ConflictException`, etc.
|
|
5. **Use custom decorators for request context** — `@CurrentUser()`, `@CurrentHousehold()`
|
|
|
|
### Web (Next.js)
|
|
|
|
1. **App Router conventions** — `page.tsx`, `layout.tsx`, `loading.tsx`, `error.tsx`
|
|
2. **Server Components by default** — only add `'use client'` when interactive
|
|
3. **Feature components** in `components/features/` — organized by domain (products, pantry, etc.)
|
|
4. **UI components** in `components/ui/` — generic, reusable, no domain logic
|
|
5. **Hooks** in `hooks/` — custom hooks for data fetching, state management
|
|
6. **Services** in `services/` — API client wrappers
|
|
|
|
### Shared
|
|
|
|
1. **Only pure TypeScript** — no Node.js, no browser, no framework code
|
|
2. **Types, enums, Zod schemas, pure utils** — nothing else
|
|
3. **Barrel exports** — every directory has `index.ts`
|
|
4. **Use `type` modifier 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.
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```markdown
|
|
## 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
|
|
|
|
```typescript
|
|
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 exceptions
|
|
- `warn` — recoverable issues, deprecated usage, slow queries
|
|
- `log` — 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)
|