File uploads are a common requirement in modern web applications — whether it’s profile pictures, documents, or media files. In this tutorial, we’ll build a file upload API with NestJS, using Multer for handling multipart/form-data requests and AWS S3 for storing uploaded files securely in the cloud.
By the end of this guide, you’ll have a working NestJS REST API that can:
-
Accept file uploads through an HTTP endpoint
-
Validate and process files with Multer
-
Upload and store files in an Amazon S3 bucket
-
Return a public file URL for access
This step-by-step guide is beginner-friendly but also follows best practices, making it suitable for production-ready applications.
Prerequisites
Before we dive into building the file upload API, make sure you have the following in place:
-
Node.js and npm/yarn
-
Install Node.js (version 18 or later is recommended).
-
npm comes bundled with Node.js, or you can use Yarn.
-
-
NestJS CLI
-
Install globally to scaffold a new NestJS project quickly:
npm install -g @nestjs/cli
-
-
Basic Knowledge of NestJS
-
You should be familiar with controllers, services, and modules.
-
Don’t worry if you’re new — we’ll explain the important parts.
-
-
AWS Account
-
You’ll need an AWS account to use Amazon S3.
-
Create an S3 bucket where uploaded files will be stored.
-
Generate an Access Key ID and Secret Access Key with the right permissions (we’ll configure this later).
-
-
API Testing Tool
-
Use Postman or cURL to test file uploads.
-
Once you have these prerequisites ready, you’re all set to start building the project.
Setting Up a New NestJS Project
We’ll start by creating a fresh NestJS project using the Nest CLI.
1. Create a New Project
Run the following command to generate a new project:
nest new nest-file-upload
You’ll be prompted to choose a package manager. Select either npm or yarn, depending on your preference.
2. Navigate into the Project
cd nest-file-upload
3. Run the Application
Start the development server:
npm run start:dev
By default, NestJS runs on http://localhost:3000. You should see:
[Nest] 12345 - Local environment
Nest application successfully started
4. Project Structure Overview
NestJS creates a starter project with the following important files and folders:
src/
├── app.controller.ts # Handles incoming requests
├── app.module.ts # Root module
├── app.service.ts # Business logic for controller
└── main.ts # Entry point of the app
We’ll be adding an upload module with a controller and service to handle file uploads.
✅ At this point, you have a working NestJS application. Next, we’ll install and configure Multer for file uploads.
Installing and Configuring Multer
Multer is a Node.js middleware for handling multipart/form-data
, which is primarily used for file uploads. NestJS has built-in support for Multer, so integration is straightforward.
1. Install Multer
Run the following command in your project:
npm install --save @nestjs/platform-express multer
npm install --save-dev @types/multer
The @nestjs/platform-express
package provides Multer integration for NestJS.
2. Create an Upload Module
Generate a new module for file uploads:
nest g module upload
This will create upload/upload.module.ts
.
3. Create an Upload Controller
Generate a controller:
nest g controller upload
Inside upload.controller.ts
, add the following code:
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
@Controller('upload')
export class UploadController {
@Post('file')
@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}`);
},
}),
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB
},
fileFilter: (req, file, callback) => {
if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
return callback(new Error('Only image files are allowed!'), false);
}
callback(null, true);
},
}),
)
uploadFile(@UploadedFile() file: Express.Multer.File) {
return {
message: 'File uploaded successfully!',
filename: file.filename,
path: file.path,
};
}
}
4. Create an Uploads Folder
Create a folder named uploads
in the root directory:
mkdir uploads
This is a temporary storage before we move files to AWS S3.
5. Test the API
Run the server:
npm run start:dev
Use Postman to send a POST
request to:
http://localhost:3000/upload/file
-
Method: POST
-
Body → form-data → Key:
file
→ Type: File → Choose an image
If successful, you’ll get a response:
{
"message": "File uploaded successfully!",
"filename": "file-1693847293023-123456789.jpg",
"path": "uploads/file-1693847293023-123456789.jpg"
}
✅ Now we have basic file upload functionality working with Multer.
Integrating AWS S3
Amazon S3 (Simple Storage Service) is one of the most reliable ways to store files. Instead of saving uploads locally, we’ll configure NestJS to send files directly to an S3 bucket.
1. Install AWS SDK
Run the following command:
npm install @aws-sdk/client-s3
We’re using the modular AWS SDK v3, which is lighter and more efficient than v2.
2. Configure AWS Credentials
You’ll need:
-
Access Key ID
-
Secret Access Key
-
Region
-
Bucket Name
It’s best to keep these in an .env
file for security:
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_REGION=ap-southeast-1
AWS_BUCKET_NAME=my-nest-bucket
Then install dotenv so we can load environment variables:
npm install dotenv
Load it in main.ts
:
import * as dotenv from 'dotenv';
async function bootstrap() {
dotenv.config();
// existing code...
}
3. Create S3 Service
Generate a service:
nest g service upload
Now edit upload.service.ts
:
import { Injectable } from '@nestjs/common';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { extname } from 'path';
import { randomUUID } from 'crypto';
@Injectable()
export class UploadService {
private readonly s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
async uploadFile(file: Express.Multer.File) {
const bucketName = process.env.AWS_BUCKET_NAME!;
const fileExt = extname(file.originalname);
const key = `${randomUUID()}${fileExt}`;
await this.s3.send(
new PutObjectCommand({
Bucket: bucketName,
Key: key,
Body: file.buffer, // Multer stores file in memory
ContentType: file.mimetype,
}),
);
return {
message: 'File uploaded successfully!',
url: `https://${bucketName}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`,
};
}
}
4. Update Controller to Use S3 Service
Modify upload.controller.ts
:
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { UploadService } from './upload.service';
@Controller('upload')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
@Post('file')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file: Express.Multer.File) {
return this.uploadService.uploadFile(file);
}
}
5. Configure Multer to Store in Memory
By default, Multer writes files to disk. Since we want to send them directly to S3, let’s use memory storage.
Update upload.module.ts
:
import { Module } from '@nestjs/common';
import { UploadController } from './upload.controller';
import { UploadService } from './upload.service';
import { MulterModule } from '@nestjs/platform-express';
@Module({
imports: [
MulterModule.register({
storage: require('multer').memoryStorage(),
}),
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
✅ Now, when you send a POST
request to http://localhost:3000/upload/file
, the file should upload directly to S3, and you’ll get back the file’s public URL.
Building the Upload Controller and Service
We already have UploadService
and UploadController
. Let’s expand them.
1. Upload Service (Enhanced)
Update upload.service.ts
:
import { Injectable, BadRequestException } from '@nestjs/common';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { extname } from 'path';
import { randomUUID } from 'crypto';
@Injectable()
export class UploadService {
private readonly s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
private bucketName = process.env.AWS_BUCKET_NAME!;
private async uploadToS3(file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('No file provided');
}
const fileExt = extname(file.originalname);
const key = `${randomUUID()}${fileExt}`;
await this.s3.send(
new PutObjectCommand({
Bucket: this.bucketName,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
}),
);
return `https://${this.bucketName}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
}
async uploadSingleFile(file: Express.Multer.File) {
const url = await this.uploadToS3(file);
return { message: 'File uploaded successfully!', url };
}
async uploadMultipleFiles(files: Express.Multer.File[]) {
const urls = await Promise.all(files.map((file) => this.uploadToS3(file)));
return { message: 'Files uploaded successfully!', urls };
}
}
2. Upload Controller (Single & Multiple Uploads)
Update upload.controller.ts
:
import {
Controller,
Post,
UploadedFile,
UploadedFiles,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express';
import { UploadService } from './upload.service';
@Controller('upload')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
// Single file upload
@Post('file')
@UseInterceptors(FileInterceptor('file'))
async uploadFile(@UploadedFile() file: Express.Multer.File) {
return this.uploadService.uploadSingleFile(file);
}
// Multiple files upload (up to 5)
@Post('files')
@UseInterceptors(FilesInterceptor('files', 5))
async uploadFiles(@UploadedFiles() files: Express.Multer.File[]) {
return this.uploadService.uploadMultipleFiles(files);
}
}
3. Adding File Validations
NestJS with Multer allows filtering files by type and size.
Update upload.module.ts
:
import { Module } from '@nestjs/common';
import { UploadController } from './upload.controller';
import { UploadService } from './upload.service';
import { MulterModule } from '@nestjs/platform-express';
import * as multer from 'multer';
@Module({
imports: [
MulterModule.register({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 MB
fileFilter: (req, file, callback) => {
if (!file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) {
return callback(new Error('Only image files are allowed!'), false);
}
callback(null, true);
},
}),
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
This ensures:
-
Only images (
jpg
,jpeg
,png
,gif
) are allowed -
File size limit is 5 MB
4. Example Responses
Single Upload (POST /upload/file
)
{
"message": "File uploaded successfully!",
"url": "https://my-nest-bucket.s3.ap-southeast-1.amazonaws.com/abcd1234.jpg"
}
Multiple Upload (POST /upload/files
)
{
"message": "Files uploaded successfully!",
"urls": [
"https://my-nest-bucket.s3.ap-southeast-1.amazonaws.com/img1.jpg",
"https://my-nest-bucket.s3.ap-southeast-1.amazonaws.com/img2.png"
]
}
✅ Now we have a robust upload API with:
-
Single & multiple uploads
-
AWS S3 integration
-
File type & size validation
Testing the API with Postman
Once your NestJS server is running (npm run start:dev
), open Postman and follow these steps:
1. Test Single File Upload
-
Method:
POST
-
URL:
http://localhost:3000/upload/file
-
Body → form-data
-
Key:
file
(type: File) -
Select an image file (e.g.,
profile.jpg
)
-
📌 Example Response:
{
"message": "File uploaded successfully!",
"url": "https://my-nest-bucket.s3.ap-southeast-1.amazonaws.com/2e8c9fa7-3e3b-47be-bdf0-d2f1dce2a88e.jpg"
}
You can copy the url
into your browser to confirm the file is stored in S3.
2. Test Multiple File Uploads
-
Method:
POST
-
URL:
http://localhost:3000/upload/files
-
Body → form-data
-
Key:
files
(type: File) -
Add multiple file fields (e.g.,
img1.png
,img2.jpg
)
-
📌 Example Response:
{
"message": "Files uploaded successfully!",
"urls": [
"https://my-nest-bucket.s3.ap-southeast-1.amazonaws.com/1cbb4a44-abc2-47c1-bb8e-62dcbf817e9c.png",
"https://my-nest-bucket.s3.ap-southeast-1.amazonaws.com/8de9f7b5-f174-4b7a-8b62-28134afda410.jpg"
]
}
3. Test Invalid File Type
If you try uploading a .pdf
or .exe
file:
{
"statusCode": 500,
"message": "Only image files are allowed!"
}
This shows that the file filter works correctly.
4. Test Large File (> 5MB)
If you try uploading a file larger than 5 MB:
{
"statusCode": 500,
"message": "File too large"
}
Multer enforces the file size limit.
✅ At this point, you’ve fully tested single and multiple uploads, validation, and AWS S3 integration.
Conclusion
In this tutorial, you learned how to build a file upload API with NestJS, using Multer for handling multipart form data and AWS S3 for cloud storage.
We covered:
-
Setting up a new NestJS project
-
Installing and configuring Multer for uploads
-
Integrating AWS S3 with the AWS SDK v3
-
Building endpoints for single and multiple file uploads
-
Adding validation for file types and size
-
Testing the API using Postman
With this foundation, you can easily extend the API by:
-
Generating signed URLs for private file access
-
Implementing a delete endpoint to remove files from S3
-
Adding authentication with JWT to secure uploads
This approach is scalable and production-ready, making it a great choice for modern web and mobile applications that rely on file storage.
You can get the full source code on our GitHub.
That's just the basics. If you need more deep learning about Nest.js, you can take the following cheap course:
-
Learn NestJS from Scratch
-
Mastering NestJS
-
Angular & NestJS Full Stack Web Development Bootcamp 2023
-
React and NestJS: A Practical Guide with Docker
-
NestJS Masterclass - NodeJS Framework Backend Development
-
Build A TodoList with NestJS and Vue JS
-
The Complete Angular & NestJS Course
Thanks!