- Create typed DTOs (ListingDetailData, ListingSearchItem, ListingSellerItem) for repository read methods - Replace all Promise<any> and PaginatedResult<any> with concrete types in repository interface and implementation - Remove `as any` casts in search params by using Prisma enum types (TransactionType, PropertyType) - Migrate all 16 handlers from NestJS built-in exceptions to domain exceptions (NotFoundException, ValidationException, etc.) - Add CONTRIBUTING.md documenting error handling convention - All 230 tests pass, typecheck clean Co-Authored-By: Paperclip <noreply@paperclip.ing>
93 lines
3.2 KiB
Markdown
93 lines
3.2 KiB
Markdown
# Contributing Guide
|
|
|
|
## Error Handling Convention
|
|
|
|
### Overview
|
|
|
|
All application-layer error handling uses **domain exceptions** from `@modules/shared/domain/domain-exception`. Never import exception classes from `@nestjs/common` in handlers — use the project's own domain exceptions instead.
|
|
|
|
The `GlobalExceptionFilter` catches all exceptions and normalizes them into a consistent JSON response with structured error codes.
|
|
|
|
### Domain Exceptions
|
|
|
|
| Exception | HTTP Status | When to use |
|
|
|---|---|---|
|
|
| `NotFoundException(entity, id?)` | 404 | Entity not found in database |
|
|
| `ValidationException(message, details?)` | 400 | Invalid input, business rule violation, value object creation failure |
|
|
| `ConflictException(message)` | 409 | Duplicate resource, idempotency violation |
|
|
| `UnauthorizedException(message?)` | 401 | Invalid/expired credentials or tokens |
|
|
| `ForbiddenException(message?)` | 403 | Authenticated but not authorized for the action |
|
|
|
|
Import from:
|
|
|
|
```typescript
|
|
import { NotFoundException, ValidationException } from '@modules/shared/domain/domain-exception';
|
|
```
|
|
|
|
### Patterns by Layer
|
|
|
|
#### Command/Query Handlers
|
|
|
|
Handlers throw domain exceptions directly. No try-catch wrapping needed — the `GlobalExceptionFilter` handles uncaught exceptions.
|
|
|
|
```typescript
|
|
// Good: domain exception with entity context
|
|
const user = await this.userRepo.findById(id);
|
|
if (!user) throw new NotFoundException('User', id);
|
|
|
|
// Good: Result pattern from value objects
|
|
const phoneResult = Phone.create(command.phone);
|
|
if (phoneResult.isErr) {
|
|
throw new ValidationException(phoneResult.unwrapErr());
|
|
}
|
|
const phone = phoneResult.unwrap();
|
|
|
|
// Good: business rule validation
|
|
if (subscription.status === 'CANCELLED') {
|
|
throw new ValidationException('Subscription da bi huy');
|
|
}
|
|
```
|
|
|
|
#### Controllers
|
|
|
|
Controllers are thin delegation layers — they dispatch to the command/query bus and return the result. No error handling needed at the controller level.
|
|
|
|
#### Domain Services / Value Objects
|
|
|
|
Use the `Result<T, E>` pattern from `@modules/shared/domain/result`:
|
|
|
|
```typescript
|
|
static create(value: string): Result<Phone, string> {
|
|
if (!isValid(value)) return Result.err('So dien thoai khong hop le');
|
|
return Result.ok(new Phone({ value }));
|
|
}
|
|
```
|
|
|
|
Handlers consume `Result` by checking `.isErr` and throwing a `ValidationException`.
|
|
|
|
### What NOT to Do
|
|
|
|
```typescript
|
|
// Bad: NestJS built-in exceptions (missing errorCode in response)
|
|
import { NotFoundException } from '@nestjs/common';
|
|
|
|
// Bad: object-style exceptions (inconsistent with domain exception API)
|
|
throw new NotFoundException({ code: ErrorCode.NOT_FOUND, message: '...' });
|
|
|
|
// Bad: generic try-catch swallowing infrastructure errors as domain errors
|
|
try {
|
|
await this.prisma.plan.findFirst({ where: { tier } });
|
|
} catch {
|
|
throw new NotFoundException('Plan not found'); // hides DB connection errors
|
|
}
|
|
|
|
// Bad: returning Result from handlers to controllers
|
|
// Handlers should unwrap Result and throw on error
|
|
```
|
|
|
|
### Repository Return Types
|
|
|
|
All repository read methods must return explicitly typed DTOs — never `Promise<any>` or `PaginatedResult<any>`. Define read DTOs in the domain layer alongside the repository interface.
|
|
|
|
See `listing-read.dto.ts` for the canonical example.
|