Errores de dominio
En petid-api hay 35 errores de dominio, uno por fichero, repartidos por
domain/<feature>/errors/. Parece mucho ceremonial para nueve líneas cada uno, pero es lo que
permite que añadir un error nuevo no obligue a tocar nada más.
La forma
Todos extienden la misma base, que está en domain/errors/domain.error.ts y es esto entero:
export abstract class DomainError extends Error {
abstract readonly statusCode: number;
abstract readonly code: string;
}
Y un error concreto:
export class PetTagRevokedError extends DomainError {
readonly statusCode = 410;
readonly code = "PET_TAG_REVOKED";
constructor() {
// 410 Gone y no 404: la chapa existió y la fila sigue en la BD por
// trazabilidad. Lo que ya no se sirve son los datos del dueño.
super("This tag has been replaced and is no longer active");
}
}
Tres piezas:
statusCode— el código HTTP. Sí, lo decide el dominio, y es deliberado: quien conoce la diferencia entre “no existe” y “existió y ya no” es el negocio, no el controller.code— identificador estable enSCREAMING_SNAKE. Es lo que consumen los frontends para decidir qué mensaje enseñar, así que cambiarlo rompe clientes: trátalo como parte de la API.message— en inglés, como todo el código. Es para quien depura, no para el usuario final; el texto que ve una persona lo pone el frontend a partir delcode.
Quién los traduce
Un único filtro, registrado globalmente. No hay try/catch en los controllers:
@Catch(DomainError)
export class DomainErrorFilter implements ExceptionFilter {
catch(ex: DomainError, host: ArgumentsHost): void {
const res = host.switchToHttp().getResponse<Response>();
res.status(ex.statusCode).json({
statusCode: ex.statusCode,
code: ex.code,
message: ex.message,
});
}
}
Al lado vive AllExceptionsFilter, que recoge lo inesperado. La diferencia de intención: si es un
DomainError, es una situación prevista del negocio; cualquier otra cosa es un fallo.
Dónde se lanzan
En el dominio y en la aplicación, nunca en la presentación:
const pet = await this.pets.findById(petId);
if (!pet) throw new PetNotFoundError(petId); // en el use case o el service
pet.assertAccessibleBy(requester); // o dentro de la propia entidad
Si un controller está lanzando un error de negocio, esa comprobación está en la capa equivocada.
Al añadir uno
- Un fichero por error, en
domain/<feature>/errors/, nombre<algo>.error.ts. - Elige el código HTTP pensando en qué le dice al cliente, y si no es el obvio, escribe por qué en un comentario — como el 410 de arriba. Ese comentario es lo que evita que alguien lo “corrija” a 404 dentro de seis meses.
- Si el error lleva datos (un id), pásalos por constructor y métetelos en el mensaje.
- No reutilices un error de otra feature porque el código HTTP coincida.
PetNotFoundErroryReportNotFoundErrorson los dos 404 y son dos clases distintas a propósito: elcodeque viaja al frontend es distinto.