963 markdown files reformatted with the repository's pinned prettier so pnpm format:check covers the folded tree like every other repo file. The formatter's embedded-language pass also normalized code fences (TS semicolons, closed HTML tags in examples, lowercased CSS hex colors, one renumbered list that skipped an index). Alphanumeric token deltas vs the fold commit were audited file-by-file; all are formatter-equivalent markup normalizations plus the four sanitized skills.
4.3 KiB
4.3 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Use DTOs and Serialization for API Responses | MEDIUM | Response DTOs prevent accidental data exposure and ensure consistency | api, dto, serialization, class-transformer |
Use DTOs and Serialization for API Responses
Never 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.
Incorrect (returning entities directly or manual spreading):
// Return entities directly
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
// Returns: { id, email, passwordHash, ssn, internalNotes, ... }
// Exposes sensitive data!
}
}
// Manual object spreading (error-prone)
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findById(id);
return {
id: user.id,
email: user.email,
name: user.name,
// Easy to forget to exclude sensitive fields
// Hard to maintain across endpoints
};
}
Correct (use class-transformer with @Exclude and response DTOs):
// Enable class-transformer globally
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.listen(3000);
}
// Entity with serialization control
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
email: string;
@Column()
name: string;
@Column()
@Exclude() // Never include in responses
passwordHash: string;
@Column({ nullable: true })
@Exclude()
ssn: string;
@Column({ default: false })
@Exclude({ toPlainOnly: true }) // Exclude from response, allow in requests
isAdmin: boolean;
@CreateDateColumn()
createdAt: Date;
@Column()
@Exclude()
internalNotes: string;
}
// Now returning entity is safe
@Controller('users')
export class UsersController {
@Get(':id')
async findOne(@Param('id') id: string): Promise<User> {
return this.usersService.findById(id);
// Returns: { id, email, name, createdAt }
// Sensitive fields excluded automatically
}
}
// For different response shapes, use explicit DTOs
export class UserResponseDto {
@Expose()
id: string;
@Expose()
email: string;
@Expose()
name: string;
@Expose()
@Transform(({ obj }) => obj.posts?.length || 0)
postCount: number;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
export class UserDetailResponseDto extends UserResponseDto {
@Expose()
createdAt: Date;
@Expose()
@Type(() => PostResponseDto)
posts: PostResponseDto[];
}
// Controller with explicit DTOs
@Controller('users')
export class UsersController {
@Get()
@SerializeOptions({ type: UserResponseDto })
async findAll(): Promise<UserResponseDto[]> {
const users = await this.usersService.findAll();
return users.map((u) => plainToInstance(UserResponseDto, u));
}
@Get(':id')
async findOne(@Param('id') id: string): Promise<UserDetailResponseDto> {
const user = await this.usersService.findByIdWithPosts(id);
return plainToInstance(UserDetailResponseDto, user, {
excludeExtraneousValues: true,
});
}
}
// Groups for conditional serialization
export class UserDto {
@Expose()
id: string;
@Expose()
name: string;
@Expose({ groups: ['admin'] })
email: string;
@Expose({ groups: ['admin'] })
createdAt: Date;
@Expose({ groups: ['admin', 'owner'] })
settings: UserSettings;
}
@Controller('users')
export class UsersController {
@Get()
@SerializeOptions({ groups: ['public'] })
async findAllPublic(): Promise<UserDto[]> {
// Returns: { id, name }
}
@Get('admin')
@UseGuards(AdminGuard)
@SerializeOptions({ groups: ['admin'] })
async findAllAdmin(): Promise<UserDto[]> {
// Returns: { id, name, email, createdAt }
}
@Get('me')
@SerializeOptions({ groups: ['owner'] })
async getProfile(@CurrentUser() user: User): Promise<UserDto> {
// Returns: { id, name, settings }
}
}
Reference: NestJS Serialization