MeshiTrack/docs/instructions/conventions.md

260 lines
9.2 KiB
Markdown
Raw Normal View History

2026-03-27 14:50:34 +09:00
# 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 |
## 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)