Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
40 prioritized NestJS best practices across architecture, DI, security, performance, testing, and microservices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
rules/api-use-dto-serialization.md
1---2title: Use DTOs and Serialization for API Responses3impact: MEDIUM4impactDescription: Response DTOs prevent accidental data exposure and ensure consistency5tags: api, dto, serialization, class-transformer6---78## Use DTOs and Serialization for API Responses910Never return entity objects directly from controllers. Use response DTOs with class-transformer's `@Exclude()` and `@Expose()` decorators to control exactly what data is sent to clients. This prevents accidental exposure of sensitive fields and provides a stable API contract.1112**Incorrect (returning entities directly or manual spreading):**1314```typescript15// Return entities directly16@Controller('users')17export class UsersController {18@Get(':id')19async findOne(@Param('id') id: string): Promise<User> {20return this.usersService.findById(id);21// Returns: { id, email, passwordHash, ssn, internalNotes, ... }22// Exposes sensitive data!23}24}2526// Manual object spreading (error-prone)27@Get(':id')28async findOne(@Param('id') id: string) {29const user = await this.usersService.findById(id);30return {31id: user.id,32email: user.email,33name: user.name,34// Easy to forget to exclude sensitive fields35// Hard to maintain across endpoints36};37}38```3940**Correct (use class-transformer with @Exclude and response DTOs):**4142```typescript43// Enable class-transformer globally44async function bootstrap() {45const app = await NestFactory.create(AppModule);46app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));47await app.listen(3000);48}4950// Entity with serialization control51@Entity()52export class User {53@PrimaryGeneratedColumn('uuid')54id: string;5556@Column()57email: string;5859@Column()60name: string;6162@Column()63@Exclude() // Never include in responses64passwordHash: string;6566@Column({ nullable: true })67@Exclude()68ssn: string;6970@Column({ default: false })71@Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests72isAdmin: boolean;7374@CreateDateColumn()75createdAt: Date;7677@Column()78@Exclude()79internalNotes: string;80}8182// Now returning entity is safe83@Controller('users')84export class UsersController {85@Get(':id')86async findOne(@Param('id') id: string): Promise<User> {87return this.usersService.findById(id);88// Returns: { id, email, name, createdAt }89// Sensitive fields excluded automatically90}91}9293// For different response shapes, use explicit DTOs94export class UserResponseDto {95@Expose()96id: string;9798@Expose()99email: string;100101@Expose()102name: string;103104@Expose()105@Transform(({ obj }) => obj.posts?.length || 0)106postCount: number;107108constructor(partial: Partial<User>) {109Object.assign(this, partial);110}111}112113export class UserDetailResponseDto extends UserResponseDto {114@Expose()115createdAt: Date;116117@Expose()118@Type(() => PostResponseDto)119posts: PostResponseDto[];120}121122// Controller with explicit DTOs123@Controller('users')124export class UsersController {125@Get()126@SerializeOptions({ type: UserResponseDto })127async findAll(): Promise<UserResponseDto[]> {128const users = await this.usersService.findAll();129return users.map(u => plainToInstance(UserResponseDto, u));130}131132@Get(':id')133async findOne(@Param('id') id: string): Promise<UserDetailResponseDto> {134const user = await this.usersService.findByIdWithPosts(id);135return plainToInstance(UserDetailResponseDto, user, {136excludeExtraneousValues: true,137});138}139}140141// Groups for conditional serialization142export class UserDto {143@Expose()144id: string;145146@Expose()147name: string;148149@Expose({ groups: ['admin'] })150email: string;151152@Expose({ groups: ['admin'] })153createdAt: Date;154155@Expose({ groups: ['admin', 'owner'] })156settings: UserSettings;157}158159@Controller('users')160export class UsersController {161@Get()162@SerializeOptions({ groups: ['public'] })163async findAllPublic(): Promise<UserDto[]> {164// Returns: { id, name }165}166167@Get('admin')168@UseGuards(AdminGuard)169@SerializeOptions({ groups: ['admin'] })170async findAllAdmin(): Promise<UserDto[]> {171// Returns: { id, name, email, createdAt }172}173174@Get('me')175@SerializeOptions({ groups: ['owner'] })176async getProfile(@CurrentUser() user: User): Promise<UserDto> {177// Returns: { id, name, settings }178}179}180```181182Reference: [NestJS Serialization](https://docs.nestjs.com/techniques/serialization)183