文件上传:使用 multer 和 @nestjs/platform-express 轻松搞定
大家好,今天我们来聊聊 Web 开发中绕不开的一个功能——文件上传。无论是用户头像、图片分享,还是 Excel 导入,都需要处理文件上传。在 NestJS 中,文件上传的实现非常方便,因为底层直接集成了 Express 生态里最流行的 multer 中间件。你只需要引入几个内置的拦截器,就能轻松接收和处理上传的文件。今天我们就来完整地走一遍文件上传的流程。
一、文件上传的基本知识
在 HTTP 协议中,文件上传通常使用 multipart/form-data 格式的请求。这种格式会将请求体分成多个部分,每个部分有自己的头部和内容,文件数据放在其中一个或多个部分中。这种格式的好处是可以同时传输文件和普通的表单字段(比如文本输入)。
NestJS 通过 @nestjs/platform-express 包提供了对 multer 的封装,让你可以用拦截器的方式来处理文件上传。
二、安装依赖
如果你用的是默认的 Express 适配器,multer 已经作为 @nestjs/platform-express 的依赖被安装了,所以你不需要额外安装。但如果需要类型定义,可以安装 @types/multer:
npm install --save-dev @types/multer三、单文件上传
最常见的场景是上传单个文件。Nest 提供了 FileInterceptor 拦截器,它会从请求中取出指定字段的文件,然后挂到 req.file 上。你可以通过 @UploadedFile() 装饰器获取这个文件对象。
步骤:
- 在控制器方法上使用
@UseInterceptors(FileInterceptor('file')),其中'file'是表单中文件字段的名称。 - 方法参数中使用
@UploadedFile() file: Express.Multer.File来接收文件。 - 其他非文件字段仍然通过
@Body()获取。
import { Controller, Post, UseInterceptors, UploadedFile, Body } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('upload')
export class UploadController {
@Post('single')
@UseInterceptors(FileInterceptor('file'))
uploadSingle(@UploadedFile() file: Express.Multer.File, @Body() body) {
console.log(file);
return { message: '文件上传成功', filename: file.originalname };
}
}默认情况下,multer 会把文件保存在内存中(作为 buffer),如果你想保存到磁盘,可以配置 dest 或 storage 选项。
配置保存路径
FileInterceptor 可以接受第二个参数,是一个 multer 的选项对象:
@UseInterceptors(FileInterceptor('file', { dest: './uploads' }))这样文件就会自动保存到 uploads 目录下,文件名是随机生成的。如果你想自定义文件名或存储逻辑,可以使用 diskStorage:
import { diskStorage } from 'multer';
import { extname } from 'path';
@UseInterceptors(FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, callback) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
callback(null, file.fieldname + '-' + uniqueSuffix + ext);
},
}),
}))四、多文件上传(同一个字段多个文件)
如果前端上传多个文件,且使用同一个字段名(比如 <input type="file" multiple> 的 name="files"),你需要用 FilesInterceptor(注意是复数)。
@Post('multiple')
@UseInterceptors(FilesInterceptor('files', 10)) // 最多10个文件
uploadMultiple(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
console.log(files);
return { message: '多文件上传成功', count: files.length };
}FilesInterceptor 的第一个参数是字段名,第二个参数是最大文件数量限制(可选)。
五、多字段文件上传
有时候一个表单里可能有多个不同的文件字段,比如头像字段 avatar 和背景图字段 background。这时候可以用 FileFieldsInterceptor,它允许你指定多个字段及其最大文件数。
@Post('fields')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
]))
uploadFields(
@UploadedFiles() files: { avatar?: Express.Multer.File[], background?: Express.Multer.File[] },
@Body() body,
) {
console.log(files.avatar?.[0]);
console.log(files.background?.[0]);
return { message: '多字段上传成功' };
}@UploadedFiles() 返回的是一个对象,键是字段名,值是对应文件数组(即使 maxCount:1 也是数组,需要取第 0 项)。
六、任意字段文件上传(不指定字段名)
如果你不知道前端会传什么字段,或者想处理所有上传的文件,可以用 AnyFilesInterceptor。它会捕获请求中的所有文件,不管字段名是什么。
@Post('any')
@UseInterceptors(AnyFilesInterceptor({ dest: 'uploads/' }))
uploadAny(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
console.log(files); // 所有文件都在这个数组里
return { message: '任意文件上传成功' };
}文件里给的例子就是用的 AnyFilesInterceptor。
七、文件对象的属性
通过 @UploadedFile() 或 @UploadedFiles() 拿到的文件对象是标准的 multer 文件对象,包含以下常用属性:
fieldname:表单字段名originalname:原始文件名encoding:文件编码mimetype:文件 MIME 类型size:文件大小(字节)destination:保存路径(如果配置了dest)filename:保存后的文件名path:完整路径buffer:文件内容的 Buffer(如果没有保存到磁盘)
八、文件验证和限制
你可以通过 multer 的 limits 选项来限制文件大小等:
@UseInterceptors(FileInterceptor('file', {
limits: { fileSize: 2 * 1024 * 1024 }, // 限制 2MB
}))如果需要根据文件类型进行过滤,可以使用 fileFilter 函数:
@UseInterceptors(FileInterceptor('file', {
fileFilter: (req, file, callback) => {
if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
return callback(new Error('只允许上传图片文件'), false);
}
callback(null, true);
},
}))注意:fileFilter 中如果回调错误,会抛给 Nest 的异常过滤器。你可以自定义异常过滤器来捕获这些错误并返回友好的响应。
九、结合 DTO 和 ValidationPipe
有时候你可能需要同时验证非文件字段,比如上传图片时附带一个描述字段。你可以定义一个 DTO,然后在 @Body() 中使用,配合 ValidationPipe:
export class UploadDto {
@IsString()
@IsOptional()
description: string;
}
@Post('single')
@UseInterceptors(FileInterceptor('file'))
uploadSingle(
@UploadedFile() file: Express.Multer.File,
@Body() body: UploadDto,
) {
// body 已经过 ValidationPipe 验证
}记得在全局或控制器上启用 ValidationPipe。
十、错误处理
当上传出错时(比如文件太大、文件类型不对、超过文件数量限制),multer 会抛出错误。这些错误会被 Nest 的异常层捕获,默认返回 500 错误。你可以通过自定义异常过滤器来捕获这些错误,并返回更友好的响应。
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
import { MulterError } from 'multer';
@Catch(MulterError)
export class MulterExceptionFilter implements ExceptionFilter {
catch(exception: MulterError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
let message = '文件上传错误';
if (exception.code === 'LIMIT_FILE_SIZE') {
message = '文件大小超过限制';
} else if (exception.code === 'LIMIT_FILE_COUNT') {
message = '文件数量超过限制';
} else if (exception.code === 'LIMIT_UNEXPECTED_FILE') {
message = '意外字段';
}
response.status(400).json({
statusCode: 400,
message,
});
}
}然后在控制器或全局使用 @UseFilters(MulterExceptionFilter)。
十一、关于文件和字段的顺序
有一点需要注意:@Body() 不能用来获取文件字段的值,因为 multipart/form-data 中文件字段是单独的部分,不会被解析到 body 中。所有文件字段都应该通过 @UploadedFile() 或 @UploadedFiles() 获取,而其他普通字段通过 @Body() 获取。
总结
NestJS 的文件上传功能基于成熟的 multer 库,通过几个内置拦截器就能轻松处理各种上传场景。你可以:
- 用
FileInterceptor处理单文件 - 用
FilesInterceptor处理同一个字段的多个文件 - 用
FileFieldsInterceptor处理多个不同字段的文件 - 用
AnyFilesInterceptor处理所有文件
同时可以通过 multer 的选项配置存储、限制、过滤,并结合 DTO 和 ValidationPipe 进行数据验证。错误处理也可以通过自定义异常过滤器来优化。

Comments | NOTHING