การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor
ซ่อนฟิลด์ที่มีข้อมูลละเอียดอ่อนและแปลงผลลัพธ์โดยใช้ @Exclude, @Expose และกลุ่มการทำซีเรียลไลซ์
การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor เป็นบทเรียน NestJS Enterprise Backend APIs ฟรีบน CoddyKit นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน NestJS Enterprise Backend APIs และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน
บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ
The Leaky Response Problem
In an enterprise API, your entity classes often carry fields the client must never see: password, refreshToken, internal flags, audit columns.
If you return the raw object straight from your service, NestJS serializes every property to JSON. A single forgotten field becomes a security incident.
- Goal: shape what leaves the server without rewriting each controller by hand.
- Tool: the
ClassSerializerInterceptorcombined withclass-transformerdecorators.
class User {
id: number;
email: string;
password: string; // never expose this!
}
const user: User = {
id: 1,
email: 'ada@corp.io',
password: 'hashed$2b$10$secret',
};
// Naive JSON.stringify leaks everything
console.log(JSON.stringify(user));How ClassSerializerInterceptor Works
ClassSerializerInterceptor intercepts the value your handler returns and runs it through class-transformer's instanceToPlain() (a.k.a. classToPlain).
Two conditions must hold for it to do anything useful:
- The returned value must be an instance of a class (not a plain object literal).
- That class must be decorated with
class-transformerdecorators like@Exclude()or@Expose().
If you return a plain {} object, the interceptor has no metadata to act on and passes it through unchanged.
Enabling the Interceptor Globally
You can bind the interceptor at three scopes: globally, per controller, or per handler. For enterprise APIs, binding it globally in main.ts guarantees consistent serialization everywhere.
It needs the Reflector so it can read decorator metadata, which is why we resolve it from the app container.
import { NestFactory, Reflector } from '@nestjs/core';
import { ClassSerializerInterceptor } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalInterceptors(
new ClassSerializerInterceptor(app.get(Reflector)),
);
await app.listen(3000);
}
bootstrap();Hiding Fields with @Exclude
The simplest way to hide a sensitive field is to decorate it with @Exclude(). The serializer will omit it from the plain output.
Make sure your service returns a real instance (e.g. new UserEntity(...) or a TypeORM entity), otherwise the decorator metadata is never applied.
@Exclude()on a property = drop it from every response.- Apply it once on the entity; every endpoint returning that entity is protected.
import { Exclude } from 'class-transformer';
export class UserEntity {
id: number;
email: string;
@Exclude()
password: string;
@Exclude()
refreshToken: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}Seeing instanceToPlain in Action
You do not need NestJS to understand the mechanics. class-transformer's instanceToPlain() is exactly what the interceptor calls under the hood.
Here the password field disappears from the serialized output because of @Exclude(), while id and email survive.
import 'reflect-metadata';
import { Exclude, instanceToPlain } from 'class-transformer';
class UserEntity {
id: number;
email: string;
@Exclude()
password: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}
const user = new UserEntity({
id: 1,
email: 'ada@corp.io',
password: 'topsecret',
});
console.log(instanceToPlain(user));
// { id: 1, email: 'ada@corp.io' }Whitelisting with @Expose
@Exclude() is opt-out. The opposite strategy is opt-in: exclude everything by default and explicitly @Expose() only the safe fields.
Set @Exclude() at the class level, then mark each public property with @Expose(). New columns added later stay hidden unless you deliberately expose them, which is the safer default for sensitive data.
import 'reflect-metadata';
import { Exclude, Expose, instanceToPlain } from 'class-transformer';
@Exclude()
class AccountEntity {
@Expose() id: number;
@Expose() email: string;
password: string; // hidden by class-level @Exclude
internalRiskScore: number; // also hidden
constructor(p: Partial<AccountEntity>) {
Object.assign(this, p);
}
}
const acc = new AccountEntity({
id: 7, email: 'grace@corp.io',
password: 'x', internalRiskScore: 42,
});
console.log(instanceToPlain(acc));
// { id: 7, email: 'grace@corp.io' }Renaming and Computing Fields with @Expose
@Expose() does more than whitelist. It can rename a property in the output via { name: 'apiName' }, and it can expose a getter as a computed field.
This lets the API surface differ from the internal model without leaking how your columns are named.
import 'reflect-metadata';
import { Expose, instanceToPlain } from 'class-transformer';
class ProfileEntity {
@Expose({ name: 'userId' })
id: number;
firstName: string;
lastName: string;
@Expose()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
constructor(p: Partial<ProfileEntity>) {
Object.assign(this, p);
}
}
const prof = new ProfileEntity({ id: 9, firstName: 'Alan', lastName: 'Turing' });
console.log(instanceToPlain(prof));
// { userId: 9, firstName: 'Alan', lastName: 'Turing', fullName: 'Alan Turing' }Serialization Groups
Sometimes a field should be visible to an admin but hidden from a normal user. Serialization groups solve this with one entity definition.
Tag properties with @Expose({ groups: ['admin'] }). The field only appears when the serializer runs with that group active.
- No group active = grouped fields are excluded.
- Group active = grouped fields are included.
import { Exclude, Expose } from 'class-transformer';
export class UserEntity {
@Expose() id: number;
@Expose() email: string;
@Exclude()
password: string;
// visible only when serialized with the 'admin' group
@Expose({ groups: ['admin'] })
internalNotes: string;
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial);
}
}Activating Groups per Handler
In NestJS you choose which groups are active using the @SerializeOptions() decorator on a controller or a handler. The interceptor passes those options into instanceToPlain().
An admin-only route activates the admin group, so internalNotes is included only there. The same UserEntity serves both audiences.
import { Controller, Get, SerializeOptions } from '@nestjs/common';
@Controller('users')
export class UsersController {
// default route: 'admin' group NOT active -> internalNotes hidden
@Get(':id')
findOne() {
return this.users.findOne();
}
@SerializeOptions({ groups: ['admin'] })
@Get('admin/:id')
findOneAsAdmin() {
return this.users.findOne(); // internalNotes now exposed
}
}Reproducing Group Behavior Standalone
Run the group logic yourself to internalize it. Passing { groups: ['admin'] } to instanceToPlain() reveals the grouped field; omitting it keeps the field hidden.
This is precisely the toggle @SerializeOptions() controls inside the framework.
import 'reflect-metadata';
import { Exclude, Expose, instanceToPlain } from 'class-transformer';
class UserEntity {
@Expose() id: number;
@Exclude() password: string;
@Expose({ groups: ['admin'] }) internalNotes: string;
constructor(p: Partial<UserEntity>) { Object.assign(this, p); }
}
const u = new UserEntity({ id: 1, password: 'x', internalNotes: 'flagged' });
console.log(instanceToPlain(u));
// { id: 1 }
console.log(instanceToPlain(u, { groups: ['admin'] }));
// { id: 1, internalNotes: 'flagged' }Common Pitfalls
Most serialization bugs trace back to a handful of mistakes:
- Returning plain objects: the interceptor ignores object literals. Always return class instances.
- Forgetting Reflect metadata: standalone scripts need
import 'reflect-metadata'andemitDecoratorMetadataintsconfig. - Nested objects: a child entity is only serialized if you wrap it with
@Type(() => Child)so the transformer knows its class. - excludeExtraneousValues: enable it with a pure
@Expose()whitelist to drop any property lacking@Expose().
import { Expose, Type } from 'class-transformer';
export class AddressEntity {
@Expose() city: string;
@Exclude() geoHash: string;
}
export class CustomerEntity {
@Expose() id: number;
@Expose()
@Type(() => AddressEntity) // required for nested serialization
address: AddressEntity;
}Quick Check: Choosing the Right Strategy
A teammate added a new ssn column to UserEntity and shipped it. It leaked in the API response. The entity currently uses per-field @Exclude() only on password. What is the most robust fix to prevent future leaks of newly added sensitive fields?
Recap
You now know how to shape NestJS responses declaratively:
- ClassSerializerInterceptor runs
instanceToPlain()on returned class instances; bind it globally for consistency. - @Exclude() drops a field (opt-out); class-level
@Exclude()plus @Expose() creates a safer opt-in whitelist. - @Expose() can rename fields and expose computed getters.
- Serialization groups with
@SerializeOptions({ groups })let one entity serve different audiences (e.g. admin vs user). - Always return real class instances, and use
@Type()for nested objects.
เรียนรู้ TypeScript ด้วย AI tutor — ฟรี
เขียนและเรียกใช้โค้ดจริงในเบราว์เซอร์ของคุณ รับความช่วยเหลือทันทีจาก AI tutor 24/7 และเรียนรู้ต่อจากที่คุณหยุดบนเว็บหรือในแอป
- คอร์ส
- 20
- บทเรียน
- 76
คำถามที่พบบ่อย
บทเรียน “การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส NestJS Enterprise Backend APIs ให้อัปเกรดเป็น CoddyKit PRO คอร์ส NestJS Enterprise Backend APIs มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor”
ซ่อนฟิลด์ที่มีข้อมูลละเอียดอ่อนและแปลงผลลัพธ์โดยใช้ @Exclude, @Expose และกลุ่มการทำซีเรียลไลซ์ คุณปฏิบัติ NestJS Enterprise Backend APIs ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน NestJS Enterprise Backend APIs หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน NestJS Enterprise Backend APIs บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 3 จากทั้งหมด 4 บทเรียน
บทเรียน “การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน NestJS Enterprise Backend APIs นี้ได้ไหม
ได้ บทเรียน NestJS Enterprise Backend APIs ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การตรวจสอบ DTO ซ้อนกันและอาร์เรย์
- ตัวตรวจสอบแบบกำหนดเองและข้อจำกัดแบบอะซิงโครนัส
- การปรับรูปแบบการตอบกลับด้วย ClassSerializerInterceptor
- การตรวจสอบแบบมีเงื่อนไขและกลุ่มแบบไดนามิก