文件上传
本教程共 47 篇 · 第 29 篇 · 更新于 2026-08-09 · 约 12 分钟阅读
本节目标:掌握 NestJS 中处理文件上传的完整方案,包括单文件、多文件上传、文件验证和文件流处理。
文件上传是 Web 开发中的常见需求。上传图片、导入 Excel、上传附件,这些场景都离不开文件处理。
NestJS 对文件上传提供了开箱即用的支持。底层用的是 multer 这个中间件,专门处理 multipart/form-data 格式的数据。
安装依赖
开始之前,先装上类型定义:
npm install -D @types/multer
有了类型定义,写代码的时候就有智能提示了。
单文件上传
最简单的场景:用户上传一个文件。
import {
Controller,
Post,
UseInterceptors,
UploadedFile
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('files')
export class FilesController {
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
return {
filename: file.originalname,
size: file.size,
};
}
}
这段代码做了三件事:
@UseInterceptors(FileInterceptor('file'))- 告诉 NestJS 用文件拦截器处理请求,'file'是表单字段的名称@UploadedFile()- 从请求中提取上传的文件Express.Multer.File- 文件的类型定义
Note
FileInterceptor从@nestjs/platform-express导入,@UploadedFile从@nestjs/common导入。
理解文件对象
上传的文件是一个对象,包含了很多有用的信息:
{
fieldname: 'file', // 表单字段名
originalname: 'photo.jpg', // 原始文件名
encoding: '7bit', // 编码方式
mimetype: 'image/jpeg', // MIME 类型
destination: '/uploads', // 存储路径(如果配置了)
filename: '123456-photo.jpg',// 生成的文件名(如果配置了)
path: '/uploads/123456-photo.jpg', // 完整路径
size: 102400 // 文件大小(字节)
}
这些信息在实际开发中很有用。比如你可以用 mimetype 判断文件类型,用 size 判断文件大小。
文件验证
上传的文件不能啥都收。得限制大小、类型,不然服务器分分钟被撑爆。
使用内置验证管道
NestJS 提供了 ParseFilePipe,配合内置的验证器,能处理大部分场景:
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
ParseFilePipe,
MaxFileSizeValidator,
FileTypeValidator
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('files')
export class FilesController {
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1024 * 1024 }), // 1MB
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
}),
)
file: Express.Multer.File,
) {
return { filename: file.originalname };
}
}
验证失败会抛出 400 Bad Request 错误。
使用验证管道构建器
如果验证规则很多,可以用 ParseFilePipeBuilder,写起来更简洁:
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: 'jpeg',
})
.addMaxSizeValidator({
maxSize: 1000,
})
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
}),
)
file: Express.Multer.File,
) {
return file;
}
Tip默认情况下文件是必填的。如果想让文件可选,在
build()里加fileIsRequired: false。
自定义验证管道
内置验证器不够用?自己写一个:
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
const oneKb = 1000;
if (value.size > oneKb) {
throw new BadRequestException('文件大小超过限制');
}
return value;
}
}
用的时候直接传进去:
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(
@UploadedFile(new FileSizeValidationPipe())
file: Express.Multer.File,
) {
return file;
}
多文件上传
同名字段的多文件
上传多张图片,字段名都一样:
@Post('upload')
@UseInterceptors(FilesInterceptor('files', 10)) // 最多10个文件
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
return { count: files.length };
}
注意这里用的是 FilesInterceptor(复数)和 @UploadedFiles()(复数)。
不同字段的多文件
头像和背景图,字段名不一样:
@Post('upload')
@UseInterceptors(
FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
])
)
uploadFiles(
@UploadedFiles()
files: {
avatar?: Express.Multer.File[],
background?: Express.Multer.File[]
}
) {
console.log(files.avatar?.[0]);
console.log(files.background?.[0]);
}
任意字段的多文件
不确定前端会传什么字段名:
@Post('upload')
@UseInterceptors(AnyFilesInterceptor())
uploadFiles(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
}
Warning
AnyFilesInterceptor会接收所有文件,生产环境慎用。容易被恶意上传大量文件。
配置 Multer
全局配置
每个接口都配置一遍太麻烦。可以在模块里设置默认配置:
import { Module } from '@nestjs/common';
import { MulterModule } from '@nestjs/platform-express';
@Module({
imports: [
MulterModule.register({
dest: './uploads', // 默认存储目录
}),
],
})
export class AppModule {}
异步配置
需要从配置文件或环境变量读取配置:
MulterModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
dest: configService.get<string>('UPLOAD_DEST'),
}),
inject: [ConfigService],
})
自定义存储引擎
默认情况下,文件存在内存里。大文件得存到磁盘:
import { diskStorage } from 'multer';
import { extname } from 'path';
@Post('upload')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
// 生成唯一文件名
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
const ext = extname(file.originalname);
cb(null, `${file.fieldname}-${uniqueSuffix}${ext}`);
},
}),
})
)
uploadFile(@UploadedFile() file: Express.Multer.File) {
return { filename: file.filename };
}
Tip文件名不要直接用用户传来的
originalname,容易被注入恶意字符。用时间戳或 UUID 生成安全的文件名。
文件过滤
只允许特定类型的文件上传:
const imageFilter = (req, file, cb) => {
if (!file.mimetype.startsWith('image/')) {
return cb(new BadRequestException('只能上传图片文件'), false);
}
cb(null, true);
};
@Post('upload')
@UseInterceptors(
FileInterceptor('file', {
fileFilter: imageFilter,
})
)
uploadFile(@UploadedFile() file: Express.Multer.File) {
return file;
}
文件下载和流式传输
上传搞定了,下载也得会。
基础下载
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('files')
export class FilesController {
@Get('download')
downloadFile(@Res() res: Response) {
const file = createReadStream(join(process.cwd(), 'package.json'));
file.pipe(res);
}
}
这种方式有个问题:用了 @Res() 就拿不到拦截器了。
使用 StreamableFile
更好的方式是用 StreamableFile:
import { Controller, Get, StreamableFile } from '@nestjs/common';
import { createReadStream } from 'fs';
import { join } from 'path';
@Controller('files')
export class FilesController {
@Get('download')
getFile(): StreamableFile {
const file = createReadStream(join(process.cwd(), 'package.json'));
return new StreamableFile(file);
}
}
StreamableFile 可以接收 Buffer 或 Stream,框架会自动处理响应流。
自定义响应头
默认 Content-Type 是 application/octet-stream。想改的话:
@Get('download')
getFile(): StreamableFile {
const file = createReadStream(join(process.cwd(), 'package.json'));
return new StreamableFile(file, {
type: 'application/json',
disposition: 'attachment; filename="package.json"',
});
}
或者用 @Header() 装饰器:
@Get('download')
@Header('Content-Type', 'application/json')
@Header('Content-Disposition', 'attachment; filename="package.json"')
getFile(): StreamableFile {
const file = createReadStream(join(process.cwd(), 'package.json'));
return new StreamableFile(file);
}
Note
StreamableFile在 Express 和 Fastify 下都能用,跨平台兼容性没问题。
踩坑经验
坑1:Fastify 不支持 Multer
FileInterceptor 基于 Multer,而 Multer 只支持 Express。用 Fastify 的话,得用别的方案。
坑2:文件存在内存里
默认情况下,小文件存在内存,大文件存在临时目录。生产环境一定要配置 diskStorage,不然内存分分钟爆掉。
坑3:文件名安全
永远不要信任用户传来的文件名。用 UUID 或时间戳重新生成,避免路径遍历攻击。
坑4:文件大小限制
一定要设置文件大小限制。默认是无限大,一个恶意用户上传几个 G 的文件,服务器就挂了。
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
}
坑5:CORS 和文件上传
前端用 axios 上传文件时,如果跨域了,记得配置 CORS。而且 Content-Type 要让浏览器自动设置,不要手动设置成 multipart/form-data。
小结
文件上传在 NestJS 里不复杂。关键知识点回顾:
- 单文件用
FileInterceptor+@UploadedFile() - 多文件用
FilesInterceptor+@UploadedFiles() - 文件验证用
ParseFilePipe+ 验证器 - 配置存储引擎用
diskStorage - 文件下载用
StreamableFile
实际项目中,文件上传通常还会配合对象存储(比如阿里云 OSS、AWS S3)。这部分内容属于业务层面,NestJS 本身不内置,需要自己写或者用第三方库。
下一章我们聊聊缓存,看看怎么提升接口性能。