Nobody likes errors, but good error handling makes them bearable. There is a popular saying in backend development: “always validate, never trust the user.” Because of this, validation errors are a normal and expected pa…
Nobody likes errors, but good error handling makes them bearable. There is a popular saying in backend development: “always validate, never trust the user.” Because of this, validation errors are a normal and expected part of any API. When an API returns different error shapes, the frontend becomes harder to write and maintain. In NestJS, the goal should be simple: always return errors in the same format, no matter where they come from. This makes forms, toasts, and error messages easy to build on the frontend side.
When using class-validator and class-transformer, validation errors are very common. By default, these errors are too complex for the frontend. A global ValidationPipe can format them into a simple and predictable structure that the frontend can easily read:
app.useGlobalPipes( new ValidationPipe({ transform: true, whitelist: true, exceptionFactory: (errors) => { return new BadRequestException({ message: 'Validation failed', errors: errors.map((error) => ({ field: error.property, messages: Object.values(error.constraints || {}), })), }); }, }),);
For example, if the frontend sends an invalid request:
POST /usersContent-Type: application/json
{ "email": "not-an-email", "password": ""}
The API responds with a clear and consistent error format:
{ "message": "Validation failed", "errors": [ { "field": "email", "messages": ["email must be an email"] }, { "field": "password", "messages": ["password should not be empty"] } ]}
For application errors beyond validation, NestJS provides built-in exceptions like BadRequestException, NotFoundException, UnauthorizedException, and more. You can use these directly, or create custom exceptions if you want to include additional fields like an error code:
// the custom exception classexport class AppError extends HttpException { constructor(message: string, code: string, status = 400) { super({ message, code }, status); }}// throwing the custom exceptionif (userExists) { throw new AppError('User already exists', 'USER_ALREADY_EXISTS', 409);}
Finally, a global exception filter can make sure all errors, including unexpected ones, follow the same format. Consistency is the key when every error looks the same, the frontend becomes easier to build and maintain. Good error handling is not about perfection, but predictability, which your frontend developers will love.