Overview
The @ParsedBody decorator is a parameter decorator that injects the validated and transformed request body into your controller method parameters. Itβs specifically designed for use with CRUD operations and ensures the body has been validated and processed according to your DTO definitions.
Signature
export const ParsedBody = () => (target, key, index) => {
Reflect.defineMetadata(PARSED_BODY_METADATA, { index }, target[key]);
};
Basic Usage
import { Controller } from '@nestjs/common';
import { Crud, Override, ParsedBody, ParsedRequest, CrudRequest } from '@nestjsx/crud';
import { User } from './user.entity';
import { CreateUserDto } from './dto/create-user.dto';
import { UserService } from './user.service';
@Crud({
model: { type: User },
})
@Controller('users')
export class UserController {
constructor(public service: UserService) {}
@Override()
async createOne(
@ParsedBody() dto: CreateUserDto,
@ParsedRequest() req: CrudRequest
) {
console.log('Validated user data:', dto);
return this.service.createOne(req, dto);
}
}
With Bulk Create
import { CreateManyDto } from '@nestjsx/crud';
@Crud({
model: { type: Product },
})
@Controller('products')
export class ProductController implements CrudController<Product> {
constructor(public service: ProductService) {}
get base(): CrudController<Product> {
return this;
}
@Override('createManyBase')
async createBulk(
@ParsedBody() dto: CreateManyDto<Product>,
@ParsedRequest() req: CrudRequest
) {
console.log(`Creating ${dto.bulk.length} products`);
// Custom validation or processing
for (const product of dto.bulk) {
if (product.price < 0) {
throw new BadRequestException('Price cannot be negative');
}
}
return this.base.createManyBase(req, dto);
}
}
Adding Business Logic
@Crud({
model: { type: Post },
})
@Controller('posts')
export class PostController implements CrudController<Post> {
constructor(
public service: PostService,
private slugService: SlugService,
) {}
get base(): CrudController<Post> {
return this;
}
@Override()
async createOne(
@ParsedBody() dto: Post,
@ParsedRequest() req: CrudRequest,
@Request() request
) {
// Add computed fields
dto.slug = this.slugService.generate(dto.title);
dto.authorId = request.user.id;
dto.createdAt = new Date();
// Sanitize content
dto.content = this.sanitizeHtml(dto.content);
return this.base.createOneBase(req, dto);
}
private sanitizeHtml(html: string): string {
// Your sanitization logic
return html;
}
}
With Update Operations
@Crud({
model: { type: Task },
})
@Controller('tasks')
export class TaskController implements CrudController<Task> {
constructor(public service: TaskService) {}
get base(): CrudController<Task> {
return this;
}
@Override('updateOneBase')
async updateOne(
@ParsedRequest() req: CrudRequest,
@ParsedBody() dto: Task
) {
// Track modifications
dto.updatedAt = new Date();
// Business rule: can't change status to completed if no assignee
if (dto.status === 'completed' && !dto.assigneeId) {
throw new BadRequestException('Cannot complete task without assignee');
}
return this.base.updateOneBase(req, dto);
}
}
Integration Test Example
From the source tests:
packages/crud/test/crud.decorator.override.spec.ts:53-55
@Override('createManyBase')
createBulk(@ParsedBody() dto: CreateManyDto<TestModel>, @ParsedRequest() req: CrudRequest) {
return this.base.createManyBase(req, dto);
}
Combining with Validation
import { IsString, IsEmail, MinLength, IsOptional } from 'class-validator';
import { Transform } from 'class-transformer';
export class CreateUserDto {
@IsString()
@MinLength(2)
firstName: string;
@IsString()
@MinLength(2)
lastName: string;
@IsEmail()
@Transform(({ value }) => value.toLowerCase())
email: string;
@IsOptional()
@IsString()
bio?: string;
}
@Crud({
model: { type: User },
dto: {
create: CreateUserDto,
update: UpdateUserDto,
},
})
@Controller('users')
export class UserController implements CrudController<User> {
constructor(public service: UserService) {}
get base(): CrudController<User> {
return this;
}
@Override()
async createOne(
@ParsedBody() dto: CreateUserDto,
@ParsedRequest() req: CrudRequest
) {
// dto is already validated and transformed
console.log('Email (lowercase):', dto.email);
// Check for existing user
const exists = await this.service.findByEmail(dto.email);
if (exists) {
throw new ConflictException('Email already registered');
}
return this.base.createOneBase(req, dto);
}
}
With File Uploads
import { UseInterceptors, UploadedFile } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Crud({
model: { type: Document },
})
@Controller('documents')
export class DocumentController implements CrudController<Document> {
constructor(
public service: DocumentService,
private storageService: StorageService,
) {}
get base(): CrudController<Document> {
return this;
}
@Override()
@UseInterceptors(FileInterceptor('file'))
async createOne(
@ParsedBody() dto: Document,
@ParsedRequest() req: CrudRequest,
@UploadedFile() file: Express.Multer.File
) {
// Upload file to storage
const fileUrl = await this.storageService.upload(file);
// Add file URL to DTO
dto.fileUrl = fileUrl;
dto.fileName = file.originalname;
dto.fileSize = file.size;
return this.base.createOneBase(req, dto);
}
}
Advanced: Conditional Processing
@Crud({
model: { type: Order },
})
@Controller('orders')
export class OrderController implements CrudController<Order> {
constructor(
public service: OrderService,
private inventoryService: InventoryService,
private paymentService: PaymentService,
) {}
get base(): CrudController<Order> {
return this;
}
@Override()
async createOne(
@ParsedBody() dto: Order,
@ParsedRequest() req: CrudRequest,
@Request() request
) {
// Validate inventory
for (const item of dto.items) {
const available = await this.inventoryService.checkStock(item.productId, item.quantity);
if (!available) {
throw new BadRequestException(`Insufficient stock for product ${item.productId}`);
}
}
// Calculate totals
dto.subtotal = dto.items.reduce((sum, item) => sum + (item.price * item.quantity), 0);
dto.tax = dto.subtotal * 0.1;
dto.total = dto.subtotal + dto.tax;
// Set customer
dto.customerId = request.user.id;
// Create order
const order = await this.base.createOneBase(req, dto);
// Process payment
if (dto.paymentMethod) {
await this.paymentService.charge(order.id, dto.total, dto.paymentMethod);
}
return order;
}
}
Implementation Details
packages/crud/src/decorators/parsed-body.decorator.ts:3-5
export const ParsedBody = () => (target, key, index) => {
Reflect.defineMetadata(PARSED_BODY_METADATA, { index }, target[key]);
};
The decorator stores metadata about the parameter index, which the CRUD framework uses to inject the validated body.
The @ParsedBody decorator ensures the request body has been validated according to your DTO class using class-validator decorators. Any validation errors will be thrown before your method is called.
Always use @ParsedBody instead of the standard @Body decorator when overriding CRUD routes. This ensures proper integration with the CRUD validation pipeline.
CreateManyDto Structure
When using @ParsedBody with bulk create operations:
interface CreateManyDto<T> {
bulk: T[];
}
Example:
@Override('createManyBase')
async createBulk(
@ParsedBody() dto: CreateManyDto<Product>
) {
console.log('Number of items:', dto.bulk.length);
dto.bulk.forEach((product, index) => {
console.log(`Product ${index}:`, product);
});
return this.service.createMany(dto);
}
See Also