Skip to main content
BadRequestException is an exception used when a client’s request is invalid. It returns an HTTP 400 status code and is used for validation failures, invalid parameters, malformed requests, etc.

Basic Usage

class BadRequestException extends SoException {
  constructor(
    public message = "Bad Request",
    public payload?: unknown,
  );
}
Simple Example:
@api()
async createPost(title: string, content: string) {
  if (!title || title.trim().length === 0) {
    throw new BadRequestException("Title is required");
  }

  if (content.length > 10000) {
    throw new BadRequestException("Content cannot exceed 10,000 characters");
  }

  return this.create({ title, content });
}

Practical Examples

Validation

@api()
async updateUser(
  userId: number,
  email?: string,
  age?: number,
  phoneNumber?: string
) {
  const errors: Array<{ field: string; message: string }> = [];

  // Email validation
  if (email && !email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
    errors.push({
      field: "email",
      message: "Invalid email format"
    });
  }

  // Age validation
  if (age !== undefined && (age < 0 || age > 150)) {
    errors.push({
      field: "age",
      message: "Age must be between 0 and 150"
    });
  }

  // Phone number validation
  if (phoneNumber && !phoneNumber.match(/^\d{3}-\d{3,4}-\d{4}$/)) {
    errors.push({
      field: "phoneNumber",
      message: "Invalid phone number format (e.g., 010-1234-5678)"
    });
  }

  if (errors.length > 0) {
    throw new BadRequestException("Invalid input data", {
      errors
    });
  }

  return this.update(userId, { email, age, phoneNumber });
}

Business Rule Validation

@api()
async transferMoney(
  ctx: Context,
  fromAccountId: number,
  toAccountId: number,
  amount: number
) {
  // Amount validation
  if (amount <= 0) {
    throw new BadRequestException(
      "Transfer amount must be greater than 0",
      { amount }
    );
  }

  if (amount > 10000000) {
    throw new BadRequestException(
      "Single transfer limit is 10 million",
      { amount, limit: 10000000 }
    );
  }

  // Account validation
  if (fromAccountId === toAccountId) {
    throw new BadRequestException("Cannot transfer to the same account");
  }

  const fromAccount = await AccountModel.findById(fromAccountId);

  if (fromAccount.userId !== ctx.user!.id) {
    throw new BadRequestException("Can only transfer from your own account");
  }

  if (fromAccount.balance < amount) {
    throw new BadRequestException(
      "Insufficient balance",
      {
        balance: fromAccount.balance,
        requested: amount,
        shortage: amount - fromAccount.balance
      }
    );
  }

  // Execute transfer
  return this.executeTransfer(fromAccountId, toAccountId, amount);
}

File Upload Validation

@upload()
async uploadProfileImage(ctx: Context) {
  const { files } = Sonamu.getContext();
  const file = files?.[0]; // Use first file

  if (!file) {
    throw new BadRequestException("File is required");
  }

  // File size validation (5MB)
  const maxSize = 5 * 1024 * 1024;
  if (file.size > maxSize) {
    throw new BadRequestException(
      "File size cannot exceed 5MB",
      {
        size: file.size,
        maxSize,
        sizeMB: (file.size / 1024 / 1024).toFixed(2),
        maxSizeMB: 5
      }
    );
  }

  // File type validation
  const allowedTypes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
  if (!allowedTypes.includes(file.mimetype)) {
    throw new BadRequestException(
      "Unsupported image format",
      {
        provided: file.mimetype,
        allowed: allowedTypes
      }
    );
  }

  // Save image
  return Sonamu.storage.save({
    file: file.buffer,
    filename: file.filename,
    bucket: "profile-images"
  });
}

Date/Time Validation

@api()
async createReservation(
  userId: number,
  startDate: Date,
  endDate: Date,
  guestCount: number
) {
  const now = new Date();

  // Check for past dates
  if (startDate < now) {
    throw new BadRequestException(
      "Reservation start date must be after current time",
      { startDate, now }
    );
  }

  // Check start/end date order
  if (startDate >= endDate) {
    throw new BadRequestException(
      "End date must be after start date",
      { startDate, endDate }
    );
  }

  // Check maximum reservation period (30 days)
  const maxDays = 30;
  const daysDiff = Math.ceil(
    (endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24)
  );

  if (daysDiff > maxDays) {
    throw new BadRequestException(
      `Maximum reservation period is ${maxDays} days`,
      { requestedDays: daysDiff, maxDays }
    );
  }

  // Guest count check
  if (guestCount < 1 || guestCount > 10) {
    throw new BadRequestException(
      "Guest count must be between 1 and 10",
      { guestCount }
    );
  }

  return this.create({ userId, startDate, endDate, guestCount });
}

Array/Object Validation

@api()
async createOrder(
  userId: number,
  items: Array<{ productId: number; quantity: number }>
) {
  // Empty order check
  if (!items || items.length === 0) {
    throw new BadRequestException("At least one product is required");
  }

  // Maximum order quantity check
  if (items.length > 50) {
    throw new BadRequestException(
      "Maximum of 50 products per order",
      { itemCount: items.length, maxItems: 50 }
    );
  }

  // Validate each item
  const errors: Array<{ index: number; message: string }> = [];

  items.forEach((item, index) => {
    if (!item.productId || item.productId <= 0) {
      errors.push({
        index,
        message: "Invalid product ID"
      });
    }

    if (!item.quantity || item.quantity <= 0) {
      errors.push({
        index,
        message: "Quantity must be at least 1"
      });
    }

    if (item.quantity > 999) {
      errors.push({
        index,
        message: "Maximum 999 items per product"
      });
    }
  });

  if (errors.length > 0) {
    throw new BadRequestException("Invalid order items", {
      errors
    });
  }

  return this.createOrder(userId, items);
}

Combining with Zod Validation

Sonamu automatically validates API parameters with Zod schemas, but use BadRequestException for additional business logic validation:
@api()
async createProduct(
  name: string,        // Auto-validated by Zod: string type
  price: number,       // Auto-validated by Zod: number type
  categoryId: number   // Auto-validated by Zod: number type
) {
  // Additional business logic validation
  if (price < 0) {
    throw new BadRequestException("Price must be 0 or greater");
  }

  const category = await CategoryModel.findById(categoryId);
  if (!category) {
    throw new BadRequestException(
      "Category does not exist",
      { categoryId }
    );
  }

  if (!category.isActive) {
    throw new BadRequestException(
      "Cannot add products to inactive category",
      { categoryId, categoryName: category.name }
    );
  }

  return this.create({ name, price, categoryId });
}

Payload Usage Patterns

Field-level Error List

throw new BadRequestException("Input validation failed", {
  errors: [
    { field: "email", code: "INVALID_FORMAT" },
    { field: "password", code: "TOO_SHORT", minLength: 8 }
  ]
});

Limit Information

throw new BadRequestException("File size exceeded", {
  size: file.size,
  limit: 5242880,  // 5MB in bytes
  unit: "bytes"
});

Retry Information

throw new BadRequestException("Daily request limit exceeded", {
  limit: 100,
  used: 100,
  resetsAt: new Date("2024-01-01T00:00:00Z")
});

Client Response Examples

Basic Response

{
  "statusCode": 400,
  "message": "Title is required"
}

Response with Payload

{
  "statusCode": 400,
  "message": "Invalid input data",
  "payload": {
    "errors": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "age",
        "message": "Age must be between 0 and 150"
      }
    ]
  }
}