好的,我们来探讨一个在构建 API 时非常重要,但又常常被初学者忽略的细节:序列化 (Serialization)。这就像是为你的应用配备了一位专业的“形象设计师”和“公关发言人”,确保它对外展示的信息既美观又得体。
API 的“形象设计师”:精通 NestJS 序列化技术
想象你的应用是一位顶级的秘密特工。
- 在他的大脑里(数据库和实体
Entity
),存储着关于他自己的所有信息:真实姓名、秘密代号、年龄、掌握的技能、执行过的所有任务记录、甚至还有一些不为人知的私人密码。 - 当他需要和外界联系时(API 响应),他绝不会把所有信息都和盘托出。他需要一位“公关发言人”,根据不同的交流对象(用户角色),决定哪些信息可以说,哪些信息必须保密,以及信息该以何种形式呈现。
序列化,在 NestJS 的上下文中,主要指的就是将一个对象(通常是实体类的实例)转换为另一种格式(通常是 JSON)的过程。但它远不止是简单的格式转换,它更关乎于 控制 (Control):
- 过滤 (Filtering):决定哪些属性应该被包含在最终的响应中。例如,绝不能把用户的密码哈希值返回给前端。
- 转换 (Transforming):改变属性的格式或值。例如,将数据库中的日期对象转换为一个更友好的时间戳字符串。
- 添加 (Adding):在响应中加入一些实体本身没有,但动态计算出来的属性。
NestJS 提供了强大的工具来优雅地处理序列化,其核心就是拦截器 (Interceptors) 配合 class-transformer
这个库。
1. 问题所在:不加控制的“大嘴巴”响应
让我们看看不进行序列化会发生什么。假设我们有一个 User
实体。
src/users/entities/user.entity.ts
export class User {
id: number;
firstName: string;
lastName: string;
email: string;
password; // 糟糕!这是敏感信息!
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
src/users/users.controller.ts
@Controller('users')
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string): User {
// 假设这里是从数据库获取了完整的 user 对象
return new User({
id: 1,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
password: 'hashed_password_string', // 这是一个巨大的安全漏洞!
});
}
}
当客户端访问 /users/1
时,它会收到一个包含了 password
字段的 JSON 对象。这是绝对不能接受的!
2. 第一步:聘请“公关发言人” (ClassSerializerInterceptor
)
NestJS 内置了一个专门用于序列化的拦截器:ClassSerializerInterceptor
。它的工作原理就是与 class-transformer
库提供的装饰器进行联动。
如何聘请他? 最简单的方式,就是让他成为一个全局的发言人,对所有响应进行处理。
src/main.ts
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { ClassSerializerInterceptor } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 聘请一位全局的形象设计师/公关发言人
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
await app.listen(3000);
}
bootstrap();
app.get(Reflector)
:ClassSerializerInterceptor
需要Reflector
的帮助来读取我们稍后会添加的装饰器元数据。
现在,发言人已经就位,但他还需要一本**“发言稿审核规则”**。
3. 第二步:编写“发言稿审核规则” (使用 class-transformer
装饰器)
这本规则就是我们直接写在实体类上的装饰器。class-transformer
提供了几个核心的“审核图章”:
@Exclude()
: “这个属性,无论如何都不能对外说!”@Expose()
: “这个属性,默认是保密的,但如果我明确说了要公开,那就公开它。”@Transform()
: “这个属性可以说,但在说出去之前,需要先这样包装或转换一下。”
第一步:安装 class-transformer
(如果之前没装过的话)
npm install class-transformer
第二步:用装饰器来“审核”我们的 User
实体
src/users/entities/user.entity.ts
(审核后)
import { Exclude } from 'class-transformer';
export class User {
id: number;
firstName: string;
lastName: string;
email: string;
// 盖上“绝密”图章
@Exclude()
password: string;
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
现在,当你再次启动应用并访问 /users/1
时,奇迹发生了!
响应结果:
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com"
}
password
字段被自动、干净地移除了!ClassSerializerInterceptor
看到了 @Exclude()
图章,并忠实地执行了保密指令。
4. “形象设计”进阶技巧
技巧一:白名单模式 (@Exclude()
+ @Expose()
)
默认情况下,ClassSerializerInterceptor
会包含所有没有被 @Exclude()
标记的属性。有时候,我们希望反过来:默认所有属性都保密,只公开那些被明确标记的。这种“白名单”模式更安全。
import { Exclude, Expose } from 'class-transformer';
@Exclude() // 1. 在类上使用 @Exclude(),将所有属性默认设为排除
export class User {
@Expose() // 2. 只在希望公开的属性上使用 @Expose()
id: number;
@Expose()
firstName: string;
@Expose()
lastName: string;
@Expose()
email: string;
password: string; // 这个没有 @Expose(),所以它会被排除
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
技巧二:动态添加属性 (@Expose() getter
)
有时我们需要返回一个由其他属性组合而成的新属性。我们可以通过 getter
方法和 @Expose()
来实现。
@Exclude()
export class User {
@Expose() id: number;
@Expose() firstName: string;
@Expose() lastName: string;
// ...
// 使用 getter 定义一个新属性
@Expose()
get fullName(): string {
return `${this.firstName} ${this.lastName}`;
}
constructor(partial: Partial<User>) {
Object.assign(this, partial);
}
}
响应结果将包含:
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"fullName": "John Doe"
}
技巧三:转换属性 (@Transform()
)
@Transform()
装饰器给予了我们完全的控制权,可以对属性值进行任意转换。
import { Transform } from 'class-transformer';
@Exclude()
export class User {
// ...
// 假设我们有一个角色属性,数据库存的是数字,但希望返回字符串
@Expose()
@Transform(({ value }) => {
if (value === 1) return 'admin';
if (value === 2) return 'moderator';
return 'member';
})
role: number; // 实体内部是 number
// ...
}```
如果一个用户的 `role` 是 `1`,那么最终的 JSON 响应中,`role` 字段的值将会是 `"admin"`。
**重要前提:确保返回的是类的实例**
`ClassSerializerInterceptor` 只能对一个**类的实例**起作用,因为它需要读取实例所属类上的装饰器。如果你从 Controller 返回一个普通的 JavaScript 对象 ` { ... } `,序列化将不会生效。
```typescript
// 错误示范 ❌
@Get(':id')
findOne(@Param('id') id: string) {
// 这是一个普通对象,不是 User 类的实例
return { id: 1, firstName: 'John', password: '...' };
}
// 正确示范 ✅
@Get(':id')
findOne(@Param('id') id: string): User {
const userDataFromDb = { id: 1, firstName: 'John', password: '...' };
// 必须将其转换为类的实例
return new User(userDataFromDb);
}
如果你使用 TypeORM 等 ORM,从数据库查询出的结果通常已经是实体类的实例,所以这一步是自动完成的。
总结
序列化是你 API 开发工具箱中必不可少的一环,它负责你应用对外的“门面”。
- 全局拦截 (
ClassSerializerInterceptor
):在main.ts
中全局启用ClassSerializerInterceptor
,为你应用的所有响应聘请一位“形象设计师”。 - 定义规则 (Decorators):在你的实体或 DTO 类上,使用
@Exclude()
,@Expose()
,@Transform()
等class-transformer
提供的装饰器,来精确地定义哪些数据可以展示,以及如何展示。 - 返回实例 (Return Class Instance):确保你的 Controller 始终返回类的实例,而不是普通 JavaScript 对象,这样序列化才能生效。
掌握了序列化,你就能构建出更安全、更专业、API 响应更清晰的 NestJS 应用程序。