- 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>
3.2 KiB
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:
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.
// 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:
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
// 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.