Skip to main content

Overview

The POS Nest API implements comprehensive error handling through NestJS’s built-in exception filters, validation pipes, and custom error responses. All errors are returned in a consistent JSON format.

NestJS Exception Filters

NestJS provides built-in HTTP exceptions that are automatically caught and formatted:

Built-in Exceptions

The application uses standard NestJS exceptions throughout:
import {
  BadRequestException,
  UnauthorizedException,
  ForbiddenException,
  NotFoundException,
} from '@nestjs/common';
ExceptionHTTP StatusUse Case
BadRequestException400Invalid input, validation failures
UnauthorizedException401Missing or invalid authentication
ForbiddenException403Insufficient permissions
NotFoundException404Resource not found

Validation Pipes

The application uses class-validator with NestJS’s ValidationPipe for automatic DTO validation.

Global Validation Pipe

Configured globally in main.ts:
src/main.ts
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
    }),
  );
  
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
Configuration:
  • whitelist: true - Strips properties not defined in the DTO
  • Automatically validates all incoming requests
  • Returns detailed validation error messages

DTO Validation Examples

Sign Up DTO

src/auth/dto/sign-up.dto.ts
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class SignUpDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsNotEmpty()
  @MinLength(6)
  password: string;
}
Validation rules:
  • Email must be a valid email format
  • Email cannot be empty
  • Password must be a string
  • Password must be at least 6 characters

Create Product DTO

src/products/dto/create-product.dto.ts
import { IsIn, IsInt, IsNotEmpty, IsNumber, IsString } from "class-validator";

export class CreateProductDto {
  @IsNotEmpty({message: 'The product name is required.'})
  @IsString({message: 'Invalid name.'})
  name: string;

  @IsNotEmpty({message: 'The product price is required.'})
  @IsNumber({maxDecimalPlaces: 2}, {message: 'The product price must be a number.'})
  price: number;

  @IsNotEmpty({message: 'The product image is required.'})
  image: string;

  @IsNotEmpty({message: 'The quantity cannot be empty.'})
  @IsNumber({maxDecimalPlaces: 0}, {message: 'invalid amount of inventory'})
  inventory: number;

  @IsNotEmpty({message: 'The category ID is required.'})
  @IsInt({message: 'Invalid category'})
  categoryId: number;
}
Custom error messages provide clear feedback to API consumers.

Validation Error Response

When validation fails, NestJS returns a structured error:
{
  "statusCode": 400,
  "message": [
    "email must be an email",
    "password must be longer than or equal to 6 characters"
  ],
  "error": "Bad Request"
}

Custom Pipes

ID Validation Pipe

A custom pipe ensures ID parameters are valid integers:
src/common/pipes/id-validation/id-validation.pipe.ts
import { BadRequestException, Injectable, ParseIntPipe } from '@nestjs/common';

@Injectable()
export class IdValidationPipe extends ParseIntPipe {
  constructor() {
    super({
      exceptionFactory: () => new BadRequestException('Invalid ID format'),
    });
  }
}
Usage in controllers:
src/products/products.controller.ts
import { IdValidationPipe } from '../common/pipes/id-validation/id-validation.pipe';

@Get(':id')
@Public()
findOne(@Param('id', IdValidationPipe) id: string) {
  return this.productsService.findOne(+id);
}
Error response for invalid ID:
{
  "statusCode": 400,
  "message": "Invalid ID format",
  "error": "Bad Request"
}

Authentication Errors

Missing Token

When no authentication token is provided:
src/auth/guards/supabase-auth.guard.ts
if (!token) {
  throw new UnauthorizedException('Missing authorization token');
}
Response:
{
  "statusCode": 401,
  "message": "Missing authorization token",
  "error": "Unauthorized"
}

Invalid Token

When the token is invalid or expired:
src/auth/guards/supabase-auth.guard.ts
if (error || !user) {
  throw new UnauthorizedException('Invalid or expired token');
}
Response:
{
  "statusCode": 401,
  "message": "Invalid or expired token",
  "error": "Unauthorized"
}

Authorization Errors

Insufficient Permissions

When a user lacks required roles:
src/auth/guards/roles.guard.ts
const hasRole = requiredRoles.includes(user.role);
if (!hasRole) {
  throw new ForbiddenException(
    `Access denied. Required role: ${requiredRoles.join(', ')}`,
  );
}
Response:
{
  "statusCode": 403,
  "message": "Access denied. Required role: admin",
  "error": "Forbidden"
}

Service-Level Errors

Supabase Authentication Errors

Errors from Supabase are wrapped in appropriate exceptions:
src/auth/auth.service.ts
async signUp(signUpDto: SignUpDto) {
  const { email, password } = signUpDto;

  const { data, error } = await this.supabase.auth.admin.createUser({
    email,
    password,
    app_metadata: { role: 'user' },
    email_confirm: true,
  });

  if (error) {
    throw new BadRequestException(error.message);
  }

  return {
    message: 'User created successfully',
    user: {
      id: data.user.id,
      email: data.user.email,
      role: data.user.app_metadata?.role,
    },
  };
}

Sign In Errors

src/auth/auth.service.ts
async signIn(signInDto: SignInDto) {
  const { email, password } = signInDto;

  const { data, error } = await this.supabase.auth.signInWithPassword({
    email,
    password,
  });

  if (error) {
    throw new UnauthorizedException(error.message);
  }

  return { /* ... */ };
}

Common Error Scenarios

1. Invalid Request Body

Request:
POST /products
{
  "name": "",
  "price": "invalid"
}
Response:
{
  "statusCode": 400,
  "message": [
    "The product name is required.",
    "The product price must be a number.",
    "The product image is required.",
    "The quantity cannot be empty.",
    "The category ID is required."
  ],
  "error": "Bad Request"
}

2. Missing Authentication

Request:
POST /products
# No Authorization header
Response:
{
  "statusCode": 401,
  "message": "Missing authorization token",
  "error": "Unauthorized"
}

3. Insufficient Permissions

Request:
POST /products
Authorization: Bearer <user_token>
# User has 'user' role, but 'admin' required
Response:
{
  "statusCode": 403,
  "message": "Access denied. Required role: admin",
  "error": "Forbidden"
}

4. Invalid ID Parameter

Request:
GET /products/abc
Response:
{
  "statusCode": 400,
  "message": "Invalid ID format",
  "error": "Bad Request"
}

5. File Upload Error

Request:
POST /products/upload-image
# No file attached
Response:
{
  "statusCode": 400,
  "message": "The image is required",
  "error": "Bad Request"
}
src/products/products.controller.ts
@Post('upload-image')
@Roles('admin')
@UseInterceptors(FileInterceptor('file'))
uploadImage(@UploadedFile() file: Express.Multer.File) {
  if (!file) {
    throw new BadRequestException('The image is required');
  }

  return this.uploadImageService.uploadFile(file);
}

Error Response Format

All errors follow a consistent structure:
{
  statusCode: number;      // HTTP status code
  message: string | string[]; // Error message(s)
  error: string;           // Error type name
}

Exception Flow

Best Practices

Use Appropriate Exceptions

Choose the right HTTP exception for each error scenario

Provide Clear Messages

Include descriptive error messages for API consumers

Validate Early

Use DTOs and pipes to validate data before processing

Handle All Errors

Catch and transform all errors into appropriate HTTP responses

Custom Error Handling

For more complex error handling, you can create custom exception filters:
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message,
    });
  }
}
The current implementation uses NestJS’s default exception filter. Custom filters can be added for more sophisticated error logging and formatting.

Authentication

Learn about authentication error handling

Architecture

Understand the request lifecycle

API Reference

See error responses for each endpoint

Build docs developers (and LLMs) love