首页 / NestJS 入门教程 / 响应处理

NestJS 入门教程

响应处理

本教程共 47 篇 · 第 13 篇 · 更新于 2026-08-09 · 约 9 分钟阅读

NestJS响应处理HttpCodeHeader文件下载StreamableFile拦截器

本节目标:掌握 NestJS 中处理响应的全部方式——从最简单的 return 到流式响应、文件下载、统一响应格式。

两种响应方式

NestJS 处理响应有两条路:

  1. 标准方式(推荐):直接 return 数据,框架自动处理
  2. 库特定方式:用 @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 枚举含义
200OK成功
201CREATED创建成功
204NO_CONTENT无内容
400BAD_REQUEST请求有误
401UNAUTHORIZED未认证
403FORBIDDEN无权限
404NOT_FOUND资源不存在
500INTERNAL_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 返回文件
  • 拦截器统一响应格式