Nest ships an exception hierarchy that maps to status codes, so a service says what went wrong and never touches a response object: NotFoundException is 404, ConflictException 409, ForbiddenException 403, UnauthorizedException 401, and HttpException(body, status) covers the rest. Anything else that escapes becomes 500 with no details leaked. The default payload is { "statusCode": 409, "message": "...", "error": "Conflict" }; Bookshelf publishes the envelope of The Bookshelf API, so one filter reshapes it. @Catch(HttpException) narrows a filter, and narrower filters registered closer to a handler win.
@Catch() // no argument: catch everything
export class ProblemFilter implements ExceptionFilter {
private readonly logger = new Logger('ProblemFilter'); // APP_FILTER keeps it injectable
catch(exception: unknown, host: ArgumentsHost) {
const [res, req] = [host.switchToHttp().getResponse(), host.switchToHttp().getRequest()];
const http = exception instanceof HttpException;
const status = http ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
const body = (http ? exception.getResponse() : {}) as Record<string, any>;
const many = Array.isArray(body.message); // ValidationPipe throws an array of them
if (status >= 500) this.logger.error(String(exception)); // log it, send none of it
res.status(status).json({ requestId: req.requestId,
error: { code: body.code ?? CODES[status] ?? 'error',
message: many ? 'Request validation failed' : body.message,
...(many ? { details: body.message } : {}) } }); } }That array test is how validation failures are recognized: ValidationPipe sets message to an array of strings, every other built-in exception uses one string. CODES maps status to the stable machine-readable code of Problem Details — clients branch on error.code, never on the sentence.
$ curl -s localhost:4310/api/v1/books/bk_99 # 404
{"error":{"code":"not_found","message":"No book with id bk_99"},"requestId":"b229ac72"}
$ curl -s -X POST .../books/bk_3/reviews -d '{"rating":4,...}' # a second review: 409
{"error":{"code":"conflict","message":"You have already reviewed this book"},..}An exception can also carry its own code by taking an object instead of a string, as login does: new UnauthorizedException({ code: 'bad_credentials', message: '...' }).