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
Copy
class BadRequestException extends SoException {
constructor(
public message = "Bad Request",
public payload?: unknown,
);
}
Copy
@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
Copy
@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
Copy
@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
Copy
@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
Copy
@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
Copy
@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 useBadRequestException for additional business logic validation:
Copy
@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
Copy
throw new BadRequestException("Input validation failed", {
errors: [
{ field: "email", code: "INVALID_FORMAT" },
{ field: "password", code: "TOO_SHORT", minLength: 8 }
]
});
Limit Information
Copy
throw new BadRequestException("File size exceeded", {
size: file.size,
limit: 5242880, // 5MB in bytes
unit: "bytes"
});
Retry Information
Copy
throw new BadRequestException("Daily request limit exceeded", {
limit: 100,
used: 100,
resetsAt: new Date("2024-01-01T00:00:00Z")
});
Client Response Examples
Basic Response
Copy
{
"statusCode": 400,
"message": "Title is required"
}
Response with Payload
Copy
{
"statusCode": 400,
"message": "Invalid input data",
"payload": {
"errors": [
{
"field": "email",
"message": "Invalid email format"
},
{
"field": "age",
"message": "Age must be between 0 and 150"
}
]
}
}