Serialization — response filtering, parolni yashirish

~20 daq · NestJS · Autentifikatsiya

Muammodan boshlaylik

GET /users endpointi javob qaytardi:

{
  "_id": "64a...",
  "name": "Jasur",
  "email": "jasur@gmail.com",
  "password": "$2b$10$xK9mP2vL...",
  "refreshToken": "$2b$10$aL7nQ3...",
  "deletedAt": null,
  "__v": 0
}

Hash qilingan bo'lsa ham — parol va refresh token javobda bo'lmasligi kerak. __v, deletedAt ham foydalanuvchiga kerak emas.

Har endpointda delete user.password yozish — takroriy va unutilishi mumkin. Serialization bu muammoni markaziy hal qiladi.


Serialization nima?

Serialization — ob'ektni javob (JSON) ga aylantirish jarayoni. NestJS da bu ClassSerializerInterceptor orqali boshqariladi.

Siz @Exclude() va @Expose() dekoratorlar bilan qaysi maydonlar ko'rinishini belgilaysiz — bir marta yoziladi, hamma joyda ishlaydi.


O'rnatish

npm install class-transformer

class-validator bilan birga o'rnatilgan bo'lishi mumkin — tekshiring.


Response DTO — nima qaytarilishini belgilash

// src/users/dto/user-response.dto.ts
import { Exclude, Expose, Transform } from 'class-transformer';

export class UserResponseDto {
  @Expose()
  id: string; // _id o'rniga id

  @Expose()
  name: string;

  @Expose()
  email: string;

  @Expose()
  role: string;

  @Expose()
  photo: string | null;

  @Expose()
  createdAt: Date;

  // Bu maydonlar javobda KO'RSATILMAYDI
  @Exclude()
  password: string;

  @Exclude()
  refreshToken: string;

  @Exclude()
  deletedAt: Date;

  @Exclude()
  __v: number;

  constructor(partial: Partial<UserResponseDto>) {
    Object.assign(this, partial);
  }
}

Global Interceptor ulash

// src/main.ts
import { NestFactory, Reflector } from '@nestjs/core';
import { ClassSerializerInterceptor } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global — barcha controller larda ishlaydi
  app.useGlobalInterceptors(
    new ClassSerializerInterceptor(app.get(Reflector)),
  );

  await app.listen(3000);
}

Service da DTO qaytarish

// src/users/users.service.ts
import { plainToInstance } from 'class-transformer';
import { UserResponseDto } from './dto/user-response.dto';

async findAll(): Promise<UserResponseDto[]> {
  const users = await this.userModel.find({ deletedAt: null }).exec();
  // plainToInstance — oddiy ob'ektni DTO klassiga aylantiradi
  return plainToInstance(UserResponseDto, users.map(u => u.toObject()), {
    excludeExtraneousValues: true, // ← @Expose() yo'q maydonlar o'tkazilmaydi
  });
}

async findOne(id: string): Promise<UserResponseDto> {
  const user = await this.userModel.findById(id).exec();
  if (!user) throw new NotFoundException('Topilmadi');
  return plainToInstance(UserResponseDto, user.toObject(), {
    excludeExtraneousValues: true,
  });
}

excludeExtraneousValues: true — @Expose() yo'q barcha maydonlar avtomatik tashlab yuboriladi. Bu eng xavfsiz variant.


MongoDB _id ni id ga aylantirish

MongoDB da _id ishlatiladi. Frontendga id qaytarish qulay:

// user-response.dto.ts
import { Expose, Transform } from 'class-transformer';
import { Types } from 'mongoose';

export class UserResponseDto {
  @Expose()
  @Transform(({ obj }) => obj._id?.toString()) // ← _id → id string
  id: string;

  @Expose()
  name: string;

  // ...
}

Yoki sxemada virtual qo'shish:

// user.schema.ts
@Schema({ timestamps: true, toJSON: { virtuals: true } })
export class User { ... }

UserSchema.virtual('id').get(function () {
  return this._id.toHexString(); // ← _id → id
});

@SerializeOptions — controller darajasida sozlash

Ba'zan turli endpointlar uchun turli javob kerak. Admin uchun batafsil, oddiy user uchun kam:

// src/users/dto/user-admin-response.dto.ts
export class UserAdminResponseDto extends UserResponseDto {
  @Expose()
  deletedAt: Date | null; // admin ko'radi

  @Expose()
  provider: string;

  @Expose()
  isActive: boolean;
}
// users.controller.ts
import { SerializeOptions } from '@nestjs/common';

@Controller('users')
export class UsersController {
  // Oddiy foydalanuvchi uchun
  @Get(':id')
  @SerializeOptions({ type: UserResponseDto })
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(id);
  }

  // Admin uchun — ko'proq ma'lumot
  @Get(':id/admin')
  @UseGuards(JwtAuthGuard, RolesGuard)
  @Roles('admin')
  @SerializeOptions({ type: UserAdminResponseDto })
  findOneAdmin(@Param('id') id: string) {
    return this.usersService.findOneAdmin(id);
  }
}

Tez-tez uchraydigan xatolar

toObject() chaqirmaslik

// ❌ Mongoose hujjati — class-transformer uni to'g'ri o'qiy olmaydi
const user = await this.userModel.findById(id).exec();
return plainToInstance(UserResponseDto, user); // ishlashi mumkin, lekin noaniq

// ✅ plain JavaScript ob'ektiga aylantirish
return plainToInstance(UserResponseDto, user.toObject(), {
  excludeExtraneousValues: true,
});

excludeExtraneousValues unutish

// ❌ Barcha maydonlar o'tadi — @Exclude() bo'lganlar ham
plainToInstance(UserResponseDto, user.toObject())

// ✅ Faqat @Expose() bo'lganlar o'tadi
plainToInstance(UserResponseDto, user.toObject(), {
  excludeExtraneousValues: true,
})

Massivni aylantirmaslik

// ❌ Massiv uchun alohida qayta ishlash kerak
const users = await this.userModel.find().exec();
return plainToInstance(UserResponseDto, users); // ← users Mongoose hujjatlar massivi

// ✅
return plainToInstance(
  UserResponseDto,
  users.map(u => u.toObject()), // ← har birini plain ob'ektga
  { excludeExtraneousValues: true },
);

Xulosa

Serialization — javobdan keraksiz maydonlarni bir martalik deklaratsiya bilan olib tashlash. @Exclude(), @Expose(), plainToInstance — uch narsa.

Eslab qoling:

  • @Exclude() — bu maydon javobda ko'rsatilmaydi
  • @Expose() — bu maydon ko'rsatiladi (excludeExtraneousValues: true bilan)
  • plainToInstance(Dto, obj, { excludeExtraneousValues: true }) — o'girish
  • user.toObject() — Mongoose hujjatini plain JS ga aylantirish
  • ClassSerializerInterceptor global — bir marta ulash yetarli
  • @SerializeOptions({ type: Dto }) — controller darajasida DTO tanlash

Keyingi dars

30-dars: File Upload va Static Files