响应处理
本教程共 47 篇 · 第 13 篇 · 更新于 2026-08-09 · 约 9 分钟阅读
本节目标:掌握 NestJS 中处理响应的全部方式——从最简单的 return 到流式响应、文件下载、统一响应格式。
两种响应方式
NestJS 处理响应有两条路:
- 标准方式(推荐):直接
return数据,框架自动处理 - 库特定方式:用
@Res()注入底层响应对象,自己控制
大部分时候用标准方式就够了。简单、干净、不用操心。
标准响应
直接返回数据就行:
@Get()
findAll() {
return {
message: 'Success',
data: [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
],
};
}
NestJS 自动帮你做的事:
- 对象/数组 → 序列化成 JSON
- 字符串/数字 → 直接发送
- 默认状态码 200,POST 默认 201
异步也没问题:
@Get()
async findAll() {
return await this.usersService.findAll();
}
也支持返回 RxJS Observable:
import { Observable, of } from 'rxjs';
@Get()
findAll(): Observable<any> {
return of({ message: 'Success' });
}
NestJS 会自动订阅 Observable,等流结束后返回最终值。
设置状态码
默认 GET 返回 200,POST 返回 201。想改就用 @HttpCode():
import { Post, HttpCode, HttpStatus } from '@nestjs/common';
@Post()
@HttpCode(HttpStatus.CREATED) // 201
create() {
return { message: '创建成功' };
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // 204
remove() {
return;
}
常用状态码:
| 状态码 | HttpStatus 枚举 | 含义 |
|---|---|---|
| 200 | OK | 成功 |
| 201 | CREATED | 创建成功 |
| 204 | NO_CONTENT | 无内容 |
| 400 | BAD_REQUEST | 请求有误 |
| 401 | UNAUTHORIZED | 未认证 |
| 403 | FORBIDDEN | 无权限 |
| 404 | NOT_FOUND | 资源不存在 |
| 500 | INTERNAL_SERVER_ERROR | 服务器错误 |
Tip用
HttpStatus枚举比直接写数字好——代码可读性更强,而且不容易写错。
设置响应头
用 @Header() 装饰器:
@Get()
@Header('Cache-Control', 'no-store')
@Header('X-Custom-Header', 'hello')
findAll() {
return { data: [] };
}
可以叠加多个 @Header()。
@Res() 库特定方式
需要更精细的控制时,用 @Res() 注入底层响应对象:
import { Controller, Get, Res } from '@nestjs/common';
import type { Response } from 'express';
@Controller('users')
export class UsersController {
@Get()
findAll(@Res() res: Response) {
res.status(200).json({
message: 'Success',
data: [],
});
}
}
Warning用了
@Res()之后,你必须自己调用res.json()或res.send()。不调用请求会一直挂着。而且@HttpCode()、@Header()这些装饰器都失效了。
如果只想用 @Res() 设置个 Cookie 或 Header,但还想让 NestJS 处理响应体,加 passthrough:
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.cookie('token', 'abc123');
return { data: [] }; // NestJS 照常处理
}
流式响应
大文件不适合一次性读进内存。用流式响应边读边发:
import { Readable } from 'stream';
@Get('stream')
stream(@Res() res: Response) {
const stream = new Readable({
read() {},
});
stream.push('第一块数据 ');
stream.push('第二块数据 ');
stream.push(null); // 结束
res.setHeader('Content-Type', 'text/plain');
stream.pipe(res);
}
文件下载
用 StreamableFile 返回文件:
import { Controller, Get, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('download')
export class DownloadController {
@Get()
getFile(): StreamableFile {
const file = createReadStream(join(process.cwd(), 'report.pdf'));
return new StreamableFile(file);
}
@Get('custom-name')
getCustomFile(@Res({ passthrough: true }) res: Response): StreamableFile {
const file = createReadStream(join(process.cwd(), 'report.pdf'));
res.setHeader('Content-Disposition', 'attachment; filename="report.pdf"');
return new StreamableFile(file);
}
}
Note
StreamableFile在 Express 和 Fastify 下都能用。它会自动处理流的 pipe 操作。
重定向
用 @Redirect() 装饰器:
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs() {}
动态决定重定向地址:
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version: string) {
if (version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
方法返回的对象会覆盖 @Redirect() 的参数。
统一响应格式
实际项目中,API 的响应格式通常是统一的。比如:
{
"code": 200,
"message": "Success",
"data": { ... },
"timestamp": "2026-08-09T12:00:00.000Z"
}
方式一:封装响应类
export class ResponseDto<T> {
code: number;
message: string;
data: T;
timestamp: string;
constructor(data: T, message = 'Success', code = 200) {
this.code = code;
this.message = message;
this.data = data;
this.timestamp = new Date().toISOString();
}
}
@Get()
findAll(): ResponseDto<User[]> {
const users = this.usersService.findAll();
return new ResponseDto(users);
}
每个方法都要手动包装,有点烦。
方式二:用拦截器自动包装(推荐)
import {
Injectable, NestInterceptor, ExecutionContext, CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, any>
{
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<any> {
return next.handle().pipe(
map(data => ({
code: 200,
message: 'Success',
data,
timestamp: new Date().toISOString(),
})),
);
}
}
全局应用:
app.useGlobalInterceptors(new TransformInterceptor());
之后所有控制器直接返回数据就行,拦截器会自动包装成统一格式:
@Get()
findAll() {
return [{ id: 1, name: '张三' }];
// 实际响应:
// { code: 200, message: "Success", data: [...], timestamp: "..." }
}
Tip拦截器方式是最佳实践。控制器代码保持简洁,响应格式在拦截器里统一管理。以后想改格式,只改一个地方。
分页响应
列表接口通常需要分页。定义一个通用的分页响应结构:
export class PaginatedDto<T> {
data: T[];
total: number;
page: number;
limit: number;
totalPages: number;
}
@Get()
async findAll(
@Query('page') page: number = 1,
@Query('limit') limit: number = 10,
): Promise<PaginatedDto<User>> {
const [data, total] = await this.usersService.findAndCount(page, limit);
return {
data,
total,
page,
limit,
totalPages: Math.ceil(total / limit),
};
}
CORS 配置
前后端分离时,必须配置 CORS:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: ['http://localhost:5173'],
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
await app.listen(3000);
}
bootstrap();
Note开发时
origin可以设成'*'(允许所有来源),但生产环境一定要指定具体的域名。
响应压缩
大响应体可以开启 gzip 压缩:
npm install compression
import compression from 'compression';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(compression());
await app.listen(3000);
}
bootstrap();
压缩后响应体积能减少 60%-80%,对接口性能提升明显。
小结
响应处理的核心就一句话:大多数时候直接 return 就行。
需要更多控制时:
@HttpCode()改状态码@Header()改响应头@Redirect()重定向@Res()完全自定义(但会失去框架的自动处理)StreamableFile返回文件- 拦截器统一响应格式