In many backend applications, file upload functionality is a common requirement. Whether you're handling profile pictures, document uploads, or other types of media, being able to handle file uploads efficiently is essential. In this blog post, we'll explore how to implement file upload functionality in NestJS using the powerful multer
package.
Prerequisites
Before we begin, you’ll need to have the following:
- NestJS application set up: If you haven’t already set up a NestJS project, check out NestJS documentation to get started.
- Multer: This is the middleware used for handling
multipart/form-data
, which is commonly used for uploading files.
Step 1: Install Required Packages
NestJS uses the @nestjs/platform-express
package to integrate with multer
. You’ll need to install this package to work with file uploads.
Run the following command to install @nestjs/platform-express
and multer
:
npm install @nestjs/platform-express multer
npm install -D @types/multer
Step 2: Create a File Upload Module
Create a new file upload module with the following structure:
nest g module file-upload
import { Module } from '@nestjs/common';
import { FileUploadController } from './file-upload.controller';
import { FileUploadService } from './file-upload.service';
@Module({
controllers: [FileUploadController],
providers: [FileUploadService],
})
export class FileUploadModule {}
Step 3: Create a File Upload Service
In NestJS, you can organize the file upload functionality into a service. This service will handle the logic for storing the uploaded files.
Create a new file upload service with the following structure:
nest g service file-upload
import { Injectable } from '@nestjs/common';
import { existsSync, mkdirSync } from 'fs';
import { extname } from 'path;
@Injectable()
export class FileUploadService {
constructor() {
// Create uploads directory if it doesn't exist
if (!existsSync('./uploads')) {
mkdirSync('./uploads');
}
}
async saveFile(file: Express.Multer.File): Promise<string> {
const fileName = `${Date.now()}${extname(file.originalname)}`;
return fileName;
}
getFileUrl(fileName: string): string {
return `uploads/${fileName}`;
}
}
In this simple service, we have a method called saveFile()
that generates a unique file name for the uploaded file. You can extend this method to include logic for saving the file to disk or cloud storage. Additionally, the getFileUrl()
method constructs the URL for accessing the uploaded file.
Step 4: Create a File Upload Controller
Next, we need a controller to handle HTTP requests for uploading files. In NestJS, the controller is responsible for managing routes and delegating logic to services.
nest g controller file-upload
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
BadRequestException,
MaxFileSizeValidator,
ParseFilePipe,
FileTypeValidator,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { FileUploadService } from './file-upload.service';
@Controller('file-upload')
export class FileUploadController {
constructor(private readonly fileUploadService: FileUploadService) {}
@Post('single')
@UseInterceptors(FileInterceptor('file')) // 'file' is the field name used for the file input
async uploadSingleFile(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1024 * 1024 * 5 }), // 5MB
new FileTypeValidator({ fileType: '.(png|jpeg|jpg)' }),
],
}),
)
file: Express.Multer.File,
) {
try {
const fileName = await this.fileUploadService.saveFile(file);
return {
message: 'File uploaded successfully',
fileName,
fileUrl: this.fileUploadService.getFileUrl(fileName),
};
} catch (error) {
throw new BadRequestException('Failed to upload file');
}
}
}
Here’s what happens in this controller:
- The
@Post('single')
decorator creates a POST endpoint at/file-upload/single
. - The
@UseInterceptors(FileInterceptor('file'))
decorator is used to handle the file upload and associate it with the file field in the request. - The
@UploadedFile()
decorator allows us to access the uploaded file in the controller method.ParseFilePipe
: Validates the file usingMaxFileSizeValidator
(max 5MB) andFileTypeValidator
(only.png
,.jpeg
,.jpg
).
Step 5: Configure Multer Options (Optional)
By default, multer
uses some sensible options for storing files, but you can customize these options to suit your needs. For example, you can limit the file size, file types, or specify a custom storage location.
To customize the multer settings, modify the controller like this:
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
BadRequestException,
FileTypeValidator,
MaxFileSizeValidator,
ParseFilePipe,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { FileUploadService } from './file-upload.service';
import { extname } from 'path';
@Controller('file-upload')
export class FileUploadController {
constructor(private readonly fileUploadService: FileUploadService) {}
@Post('single')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: './uploads', // Define the directory to save the files
filename: (req, file, callback) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
callback(null, `${file.fieldname}-${uniqueSuffix}${extname(file.originalname)}`);
},
}),
fileFilter: (req, file, cb) => {
if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
return cb(new Error('Only image files are allowed!'), false);
}
cb(null, true);
},
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB size limit
}),
)
async uploadSingleFile(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1024 * 1024 * 5 }), // 5MB
new FileTypeValidator({ fileType: '.(png|jpeg|jpg)' }),
],
}),
)
file: Express.Multer.File,
) {
try {
const fileName = await this.fileUploadService.saveFile(file);
return {
message: 'File uploaded successfully',
fileName,
fileUrl: this.fileUploadService.getFileUrl(fileName),
};
} catch (error) {
throw new BadRequestException('Failed to upload file');
}
}
}
In this configuration:
- The
diskStorage()
function allows us to specify the file destination and filename format. - The
fileFilter()
function is used to restrict the types of files that can be uploaded. - The
limits
option is used to specify the maximum file size (5 MB in this case).
Step 5: Handle Multiple File Uploads (Optional)
If you need to handle multiple file uploads at once, you can use the FilesInterceptor from NestJS.
import {
Controller,
Post,
UseInterceptors,
BadRequestException,
FileTypeValidator,
MaxFileSizeValidator,
ParseFilePipe,
UploadedFiles,
} from '@nestjs/common';
import { FilesInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { FileUploadService } from './file-upload.service';
import { extname } from 'path';
@Controller('file-upload')
export class FileUploadController {
constructor(private readonly fileUploadService: FileUploadService) {}
@Post('multiple')
@UseInterceptors(
FilesInterceptor('files', 10, {
storage: diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
cb(null, `${file.fieldname}-${uniqueSuffix}${extname(file.originalname)}`);
},
}),
fileFilter: (req, file, cb) => {
if (!file.originalname.match(/\.(jpg|jpeg|png)$/)) {
return cb(new Error('Only image files are allowed!'), false);
}
cb(null, true);
},
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB size limit
}),
)
async uploadMultipleFiles(
@UploadedFiles(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 1024 * 1024 * 5 }), // 5MB
new FileTypeValidator({ fileType: '.(png|jpeg|jpg)' }),
],
}),
)
files: Express.Multer.File[],
) {
try {
const uploadedFiles = await Promise.all(
files.map(async (file) => {
const fileName = await this.fileUploadService.saveFile(file);
return {
fileName,
fileUrl: this.fileUploadService.getFileUrl(fileName),
};
}),
);
return {
message: 'Files uploaded successfully',
files: uploadedFiles,
};
} catch (error) {
throw new BadRequestException('Failed to upload files');
}
}
}
Here, the FilesInterceptor is used to handle multiple files, and the @UploadedFiles() decorator allows us to access the array of uploaded files.
Step 6: Serve Uploaded Files (Optional)
If you want to serve the uploaded files back to the client, you need to set up static file serving in your application.
In the main app.module.ts
, add the following configuration to serve static files from the uploads
directory:
import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { FileUploadModule } from './file-upload/file-upload.module';
@Module({
imports: [
FileUploadModule,
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'uploads'),
serveRoot: '/uploads',
}),
],
})
export class AppModule {}
This will make the files in the uploads
directory accessible through URLs like http://localhost:3000/uploads/your-file.jpg
Step 7: Implementing Error Handling (Optional)
To handle errors during file upload, you can create a custom exception filter. This filter will catch exceptions thrown by NestJS and multer
, and return appropriate error messages to the client.
Create a new file file-upload-exception.filter.ts
with the following content:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
import { MulterError } from 'multer';
@Catch(HttpException, MulterError)
export class FileUploadExceptionFilter implements ExceptionFilter {
catch(exception: HttpException | MulterError, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = 500;
let message = 'Internal server error';
if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.message;
} else if (exception instanceof MulterError) {
status = 400;
message = this.getMulterErrorMessage(exception);
}
response.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
message,
});
}
private getMulterErrorMessage(error: MulterError): string {
switch (error.code) {
case 'LIMIT_FILE_SIZE':
return 'File size exceeds the allowed limit';
case 'LIMIT_FILE_COUNT':
return 'Too many files uploaded';
case 'LIMIT_UNEXPECTED_FILE':
return 'Unexpected field name in request';
default:
return 'File upload error';
}
}
}
To use this filter, apply it to your controller or globally in your application. For example, to apply it globally, update your main.ts
file:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { FileUploadExceptionFilter } from './file-upload/file-upload-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new FileUploadExceptionFilter());
await app.listen(3000);
}
bootstrap();
This will ensure that any errors during file upload are properly handled and returned to the client with meaningful messages.
Step 8: Test the File Upload
To test the file upload functionality, you can use tools like Postman or Insomnia. Send a POST request to http://localhost:3000/upload/single
(for a single file) or http://localhost:3000/upload/multiple
(for multiple files) and include a file in the request body.
Example Request (for Postman):
- Method: POST
- URL:
http://localhost:3000/file-upload/single
- Body: Form-data, key:
file
, value: (choose file)
Conclusion
Implementing file upload functionality in NestJS is straightforward with the help of multer
and the @nestjs/platform-express
package. We have covered uploading a single file, handling multiple files, and customizing file storage options. Additionally, we showed how to serve uploaded files back to the client. This setup can be further extended to integrate cloud storage solutions or more complex file handling logic based on your project needs.