Files
goodgo-platform/CONTRIBUTING.md
Ho Ngoc Hai e9889539ea fix: eliminate untyped repository returns and standardize DomainException usage across all handlers
- 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>
2026-04-08 06:25:44 +07:00

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.