Để xử lý việc upload file, Nest cung cấp một module dựng sẵn dựa trên middleware package multer của Express. Multer xử lý dữ liệu được gửi theo định dạng multipart/form-data, chủ yếu dùng để upload file qua HTTP request POST. Module này có thể cấu hình đầy đủ và bạn có thể tùy chỉnh hành vi của nó theo yêu cầu ứng dụng của mình.
Cảnh báo Multer không thể xử lý dữ liệu không ở định dạng multipart được hỗ trợ (multipart/form-data). Ngoài ra, lưu ý rằng package này không tương thích với FastifyAdapter.
Ghi chú người dịch Nếu bạn đã quen Express thuần, đây chính là multer mà bạn từng gắn trực tiếp làm middleware trên route (app.post('/upload', upload.single('file'), handler)). Nest chỉ bọc nó lại dưới dạng interceptor để bạn khai báo bằng decorator, cơ chế parse multipart bên dưới vẫn là multer y hệt — nên mọi giới hạn của multer (không hỗ trợ Fastify, không tự động retry, lưu file tạm trên đĩa theo mặc định) đều áp dụng nguyên vẹn ở đây.
Để tăng độ an toàn kiểu dữ liệu (type safety), hãy cài package khai báo kiểu (typings) cho Multer:
$ npm i -D @types/multer
Sau khi cài package này, ta có thể dùng kiểu Express.Multer.File (bạn có thể import kiểu này như sau: import { Express } from 'express').
Ví dụ cơ bản
Để upload một file đơn, chỉ cần gắn interceptor FileInterceptor() vào route handler và trích xuất file từ request bằng decorator @UploadedFile().
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
console.log(file);
}
@Post('upload')
@UseInterceptors(FileInterceptor('file'))
@Bind(UploadedFile())
uploadFile(file) {
console.log(file);
}
Gợi ý Decorator FileInterceptor() được export từ package @nestjs/platform-express. Decorator @UploadedFile() được export từ @nestjs/common.
Decorator FileInterceptor() nhận hai tham số:
fieldName: chuỗi cung cấp tên field trong form HTML chứa fileoptions: object tùy chọn kiểuMulterOptions. Đây là object tương tự được dùng bởi constructor của multer (xem chi tiết tại đây).
Cảnh báo FileInterceptor() có thể không tương thích với các nhà cung cấp cloud bên thứ ba như Google Firebase hay các dịch vụ khác.
Kiểm tra dữ liệu file (file validation)
Đôi khi việc kiểm tra dữ liệu metadata của file đầu vào, như kích thước file hay mime-type, sẽ hữu ích. Để làm việc này, bạn có thể tự tạo một Pipe và gắn nó vào tham số được đánh dấu bằng decorator UploadedFile. Ví dụ dưới đây minh họa cách triển khai một pipe kiểm tra kích thước file cơ bản:
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// "value" là một object chứa các thuộc tính và metadata của file
const oneKb = 1000;
return value.size < oneKb;
}
}
Pipe này có thể được dùng kết hợp với FileInterceptor như sau:
@Post('file')
@UseInterceptors(FileInterceptor('file'))
uploadFileAndValidate(@UploadedFile(
new FileSizeValidationPipe(),
// các pipe khác có thể được thêm ở đây
) file: Express.Multer.File, ) {
return file;
}
Nest cung cấp sẵn một pipe để xử lý các trường hợp sử dụng phổ biến và giúp việc thêm pipe mới trở nên dễ dàng/thống nhất hơn. Pipe này tên là ParseFilePipe, bạn có thể dùng nó như sau:
@Post('file')
uploadFileAndPassValidation(
@Body() body: SampleDto,
@UploadedFile(
new ParseFilePipe({
validators: [
// ... Tập hợp các instance file validator ở đây
]
})
)
file: Express.Multer.File,
) {
return {
body,
file: file.buffer.toString(),
};
}
Như bạn thấy, bạn cần chỉ định một mảng các file validator sẽ được ParseFilePipe thực thi. Ta sẽ bàn về interface của một validator ngay sau đây, nhưng cũng đáng nói rằng pipe này còn có thêm hai tùy chọn không bắt buộc:
errorHttpStatusCode |
Mã HTTP status sẽ được ném ra khi bất kỳ validator nào thất bại. Mặc định là 400 (BAD REQUEST) |
exceptionFactory |
Một factory nhận vào thông báo lỗi và trả về một lỗi. |
Giờ quay lại với interface FileValidator. Để tích hợp validator với pipe này, bạn phải hoặc dùng các implementation dựng sẵn, hoặc tự cung cấp một FileValidator tùy chỉnh. Xem ví dụ dưới đây:
export abstract class FileValidator<TValidationOptions = Record<string, any>> {
constructor(protected readonly validationOptions: TValidationOptions) {}
/**
* Indicates if this file should be considered valid, according to the options passed in the constructor.
* @param file the file from the request object
*/
abstract isValid(file?: any): boolean | Promise<boolean>;
/**
* Builds an error message in case the validation fails.
* @param file the file from the request object
*/
abstract buildErrorMessage(file: any): string;
}
Gợi ý Interface FileValidator hỗ trợ kiểm tra bất đồng bộ (async) thông qua hàm isValid. Để tận dụng tính an toàn kiểu dữ liệu, bạn cũng có thể khai báo kiểu cho tham số file là Express.Multer.File trong trường hợp bạn dùng express (mặc định) làm driver.
FileValidator là một class thông thường, có quyền truy cập vào object file và kiểm tra nó theo các tùy chọn được client cung cấp. Nest có sẵn hai implementation FileValidator bạn có thể dùng trong dự án của mình:
MaxFileSizeValidator- Kiểm tra xem kích thước của một file có nhỏ hơn giá trị được cung cấp hay không (tính bằngbytes)FileTypeValidator- Kiểm tra xem mime-type của một file có khớp với một chuỗi hoặc RegExp cho trước hay không. Mặc định, việc kiểm tra mime-type dựa trên magic number của nội dung file
Để hiểu cách các validator này được dùng cùng với ParseFilePipe đã nói ở trên, ta sẽ dùng một đoạn code biến thể từ ví dụ trước:
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1000 }),
new FileTypeValidator({ fileType: 'image/jpeg' }),
],
}),
)
file: Express.Multer.File,
Gợi ý Nếu số lượng validator tăng lên nhiều hoặc các tùy chọn của chúng làm file trở nên rối mắt, bạn có thể định nghĩa mảng này trong một file riêng và import nó vào đây dưới dạng một hằng số có tên, ví dụ fileValidators.
Cuối cùng, bạn có thể dùng class đặc biệt ParseFilePipeBuilder, cho phép bạn kết hợp & dựng các validator của mình. Bằng cách dùng nó như dưới đây, bạn có thể tránh việc khởi tạo thủ công từng validator và chỉ cần truyền trực tiếp các tùy chọn của chúng:
@UploadedFile(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: 'jpeg',
})
.addMaxSizeValidator({
maxSize: 1000
})
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
}),
)
file: Express.Multer.File,
Gợi ý Mặc định file bắt buộc phải tồn tại, nhưng bạn có thể làm nó thành không bắt buộc bằng cách thêm tham số fileIsRequired: false bên trong các option của hàm build (cùng cấp với errorHttpStatusCode).
Mảng file (array of files)
Để upload một mảng file (được nhận diện bằng một tên field duy nhất), dùng decorator FilesInterceptor() (lưu ý chữ Files số nhiều trong tên decorator). Decorator này nhận ba tham số:
fieldName: như đã mô tả ở trênmaxCount: số tùy chọn, xác định số lượng file tối đa được chấp nhậnoptions: objectMulterOptionstùy chọn, như đã mô tả ở trên
Khi dùng FilesInterceptor(), trích xuất file từ request bằng decorator @UploadedFiles().
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
}
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
@Bind(UploadedFiles())
uploadFile(files) {
console.log(files);
}
Gợi ý Decorator FilesInterceptor() được export từ package @nestjs/platform-express. Decorator @UploadedFiles() được export từ @nestjs/common.
Kiểm tra dữ liệu một mảng file
Để kiểm tra dữ liệu một mảng file đã upload, truyền một pipe được dựng bằng ParseFilePipeBuilder vào decorator @UploadedFiles(). Các validator đã đăng ký sẽ chạy trên từng file trong mảng.
@Post('upload')
@UseInterceptors(FilesInterceptor('files', 5))
uploadFiles(
@UploadedFiles(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: /^image\/(jpeg|png|gif)$/,
})
.addMaxSizeValidator({ maxSize: 5242880 })
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
}),
)
files: Array<Express.Multer.File>,
) {
console.log(files);
}
Nhiều file (multiple files)
Để upload nhiều file (mỗi file có một field name key khác nhau), dùng decorator FileFieldsInterceptor(). Decorator này nhận hai tham số:
uploadedFields: một mảng object, mỗi object yêu cầu một thuộc tínhnamebắt buộc với giá trị chuỗi chỉ định tên field, như đã mô tả ở trên, và một thuộc tínhmaxCounttùy chọn, như đã mô tả ở trênoptions: objectMulterOptionstùy chọn, như đã mô tả ở trên
Khi dùng FileFieldsInterceptor(), trích xuất file từ request bằng decorator @UploadedFiles().
@Post('upload')
@UseInterceptors(FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
]))
uploadFile(@UploadedFiles() files: { avatar?: Express.Multer.File[], background?: Express.Multer.File[] }) {
console.log(files);
}
@Post('upload')
@Bind(UploadedFiles())
@UseInterceptors(FileFieldsInterceptor([
{ name: 'avatar', maxCount: 1 },
{ name: 'background', maxCount: 1 },
]))
uploadFile(files) {
console.log(files);
}
Bất kỳ file nào (any files)
Để upload tất cả các field với field name key tùy ý, dùng decorator AnyFilesInterceptor(). Decorator này có thể nhận một object options tùy chọn như đã mô tả ở trên.
Khi dùng AnyFilesInterceptor(), trích xuất file từ request bằng decorator @UploadedFiles().
@Post('upload')
@UseInterceptors(AnyFilesInterceptor())
uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
console.log(files);
}
@Post('upload')
@Bind(UploadedFiles())
@UseInterceptors(AnyFilesInterceptor())
uploadFile(files) {
console.log(files);
}
Không có file (no files)
Để chấp nhận multipart/form-data nhưng không cho phép upload bất kỳ file nào, dùng NoFilesInterceptor. Cách này đặt dữ liệu multipart thành các thuộc tính trên request body. Bất kỳ file nào được gửi kèm request sẽ khiến một BadRequestException bị ném ra.
@Post('upload')
@UseInterceptors(NoFilesInterceptor())
handleMultiPartData(@Body() body) {
console.log(body)
}
Tùy chọn mặc định (default options)
Bạn có thể chỉ định các tùy chọn multer trong các file interceptor như đã mô tả ở trên. Để đặt tùy chọn mặc định, bạn có thể gọi static method register() khi import MulterModule, truyền vào các tùy chọn được hỗ trợ. Bạn có thể dùng tất cả tùy chọn được liệt kê tại đây.
MulterModule.register({
dest: './upload',
});
Gợi ý Class MulterModule được export từ package @nestjs/platform-express.
Cấu hình bất đồng bộ (async configuration)
Khi bạn cần đặt tùy chọn MulterModule bất đồng bộ thay vì tĩnh (static), dùng method registerAsync(). Cũng như hầu hết dynamic module khác, Nest cung cấp vài kỹ thuật để xử lý cấu hình bất đồng bộ.
Một kỹ thuật là dùng factory function:
MulterModule.registerAsync({
useFactory: () => ({
dest: './upload',
}),
});
Giống như các factory provider khác, factory function của chúng ta có thể là async và có thể inject dependency thông qua inject.
MulterModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
dest: configService.get<string>('MULTER_DEST'),
}),
inject: [ConfigService],
});
Ngoài ra, bạn có thể cấu hình MulterModule bằng một class thay vì factory, như dưới đây:
MulterModule.registerAsync({
useClass: MulterConfigService,
});
Cấu trúc trên khởi tạo MulterConfigService bên trong MulterModule, dùng nó để tạo object options cần thiết. Lưu ý rằng trong ví dụ này, MulterConfigService phải implement interface MulterOptionsFactory, như dưới đây. MulterModule sẽ gọi method createMulterOptions() trên object đã được khởi tạo của class được cung cấp.
@Injectable()
class MulterConfigService implements MulterOptionsFactory {
createMulterOptions(): MulterModuleOptions {
return {
dest: './upload',
};
}
}
Nếu bạn muốn tái sử dụng một options provider đã tồn tại thay vì tạo một bản sao riêng bên trong MulterModule, dùng cú pháp useExisting.
MulterModule.registerAsync({
imports: [ConfigModule],
useExisting: ConfigService,
});
Bạn cũng có thể truyền các extraProviders (provider bổ sung) cho method registerAsync(). Các provider này sẽ được gộp chung với các provider của module.
MulterModule.registerAsync({
imports: [ConfigModule],
useClass: ConfigService,
extraProviders: [MyAdditionalProvider],
});
Điều này hữu ích khi bạn muốn cung cấp thêm dependency cho factory function hoặc constructor của class.
Ghi chú người dịch Mẫu registerAsync này chính là pattern factory provider quen thuộc trong DI container — tương tự cách bạn bind một service vào Laravel service container bằng closure nhận $app để resolve dependency khác trước khi trả về instance cấu hình. Điểm hay của Nest ở đây là useExisting cho phép bạn tái dùng một provider đã có sẵn trong hệ thống (ví dụ ConfigService đã được cấu hình ở module khác) thay vì Nest phải tạo thêm một instance riêng chỉ để phục vụ MulterModule, giúp tránh trùng lặp cấu hình khi ứng dụng lớn dần.
Ví dụ
Một ví dụ hoạt động đầy đủ có sẵn tại đây.