首页 / NestJS 入门教程 / 文件上传

NestJS 入门教程

文件上传

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

NestJS文件上传Multer文件验证文件流StreamableFile

本节目标:掌握 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,
    };
  }
}

这段代码做了三件事:

  1. @UseInterceptors(FileInterceptor('file')) - 告诉 NestJS 用文件拦截器处理请求,'file' 是表单字段的名称
  2. @UploadedFile() - 从请求中提取上传的文件
  3. 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 可以接收 BufferStream,框架会自动处理响应流。

自定义响应头

默认 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 本身不内置,需要自己写或者用第三方库。

下一章我们聊聊缓存,看看怎么提升接口性能。