How to Integrate Cloudflare R2 Object Storage with NestJS for Seamless File Uploads
A production-ready guide to uploading, managing, and serving files in NestJS using Cloudflare R2 with S3-compatible APIs, secure access control, and scalable storage architecture.
Senior Developer

When building modern web applications, handling file uploads efficiently and cost-effectively is a top priority. While storing files on a local disk is fine for a quick prototype, it quickly becomes unmanageable when your app scales, especially if you deploy across multiple server instances or use ephemeral container environments like Docker or Kubernetes.
Enter Cloudflare R2—an S3-compatible object storage service that boasts a compelling feature: zero egress fees. This makes it an incredibly attractive alternative to Amazon S3 for storing and serving user-uploaded assets like avatars, screenshots, and documents.
In this tutorial, we will walk through how to seamlessly integrate Cloudflare R2 into a NestJS application, allowing you to upload files directly to the cloud without touching the local disk.
Step 1: Setting up Cloudflare R2
Before writing any code, we need to create an R2 bucket and grab our credentials.
Sign up / Log in to Cloudflare: Head over to the Cloudflare Dashboard and create an account if you don't have one.
Navigate to R2: On the left sidebar, click on R2 Object Storage. (Note: You may need to add a payment method to enable R2, but there is a generous free tier of 10 GB storage and millions of operations per month).
Create a Bucket: Click Create bucket, give it a unique name (e.g.,
my-app-uploads), and leave the default location hint (or choose one closest to your users).Enable Public Access (Optional but Recommended for Images): If you are serving images to users, you'll want to make them accessible. Go to your bucket's Settings tab, scroll down to Public Access, and either connect a Custom Domain or enable an R2.dev subdomain.
Generate API Credentials: Go back to the main R2 page and click on Manage R2 API Tokens on the right side.
Click Create API token.
Select the Object Read & Write permission.
Restrict it to the specific bucket you just created if you want better security.
Finish the creation process.
Important: Copy the Access Key ID, Secret Access Key, and Endpoint URL (Account ID). You will only see the Secret Access Key once!
Step 2: NestJS Project Setup
Assuming you have a NestJS application ready, we need to install the AWS SDK. Because Cloudflare R2 is fully S3-compatible, we can use the standard @aws-sdk/client-s3 package.
npm install @aws-sdk/client-s3
npm install -D @types/multerEnvironment Variables
Add the credentials you gathered in Step 1 to your .env file:
R2_ACCOUNT_ID=your_account_id
R2_ACCESS_KEY_ID=your_access_key
R2_SECRET_ACCESS_KEY=your_secret_key
R2_BUCKET_NAME=my-app-uploads
R2_PUBLIC_URL=https://pub-xxxxxx.r2.dev # Optional: For returning the public image URLStep 3: Creating the R2 Storage Service
We will create a service responsible for initializing the S3 client and handling the upload logic. We'll use a memory buffer so that the files are never written to your server's disk.
Create a new file r2-storage.service.ts:
import { Injectable, Logger } from '@nestjs/common';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
@Injectable()
export class R2StorageService {
private readonly logger = new Logger(R2StorageService.name);
private client: S3Client;
constructor() {
const accountId = process.env.R2_ACCOUNT_ID;
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
const secretAccessKey = process.env.R2_SECRET_ACCESS_KEY;
if (!accountId || !accessKeyId || !secretAccessKey) {
throw new Error('R2 credentials are not configured.');
}
// Initialize the S3 Client pointing to Cloudflare R2
this.client = new S3Client({
region: 'auto', // R2 requires 'auto' as the region
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
credentials: {
accessKeyId,
secretAccessKey,
},
});
}
/**
* Upload a memory buffer directly to R2 and return the public URL.
*/
async uploadBuffer(buffer: Buffer, key: string, contentType: string): Promise<string> {
const bucketName = process.env.R2_BUCKET_NAME;
try {
await this.client.send(
new PutObjectCommand({
Bucket: bucketName,
Key: key,
Body: buffer,
ContentType: contentType,
CacheControl: 'public, max-age=31536000, immutable', // Optimize caching for static assets
}),
);
const publicUrl = process.env.R2_PUBLIC_URL;
const url = publicUrl
? `${publicUrl}/${key}`
: `https://${bucketName}.${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${key}`;
this.logger.log(`Uploaded ${key} to R2 successfully.`);
return url;
} catch (error) {
this.logger.error(`Failed to upload ${key} to R2: ${error}`);
throw error;
}
}
}
Step 4: Building the Upload Controller
Now, let's create the controller that will receive the multipart/form-data upload from the client. We'll use NestJS's built-in FileInterceptor but configure it to use memoryStorage() instead of the default disk storage.
Create an upload.controller.ts file:
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import { extname } from 'path';
import { randomBytes } from 'crypto';
import { R2StorageService } from './r2-storage.service';
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5 MB
@Controller('upload')
export class UploadController {
constructor(private readonly r2Storage: R2StorageService) {}
@Post()
@UseInterceptors(
FileInterceptor('file', {
storage: memoryStorage(), // Keep file in memory, do not write to disk
limits: { fileSize: MAX_FILE_SIZE },
fileFilter: (req, file, cb) => {
const allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!allowedMimes.includes(file.mimetype)) {
return cb(new BadRequestException('Only image files are allowed!'), false);
}
cb(null, true);
},
}),
)
async uploadFile(@UploadedFile() file: Express.Multer.File) {
if (!file) {
throw new BadRequestException('File is required');
}
try {
// Generate a random, unique filename to prevent collisions
const randomName = randomBytes(16).toString('hex');
const filename = `${randomName}${extname(file.originalname)}`;
const key = `uploads/${filename}`;
// Upload the buffer straight to Cloudflare R2
const url = await this.r2Storage.uploadBuffer(
file.buffer,
key,
file.mimetype,
);
// Return the newly generated cloud URL to the client
return {
url,
filename,
originalName: file.originalname,
size: file.size,
};
} catch (error) {
throw new BadRequestException('Failed to upload file to cloud storage.');
}
}
}Make sure you declare R2StorageService as a provider in your module and UploadController in the controllers array.
Conclusion
And there you have it! By using memoryStorage(), we intercept the file and stream it directly as a buffer into Cloudflare R2. This completely eliminates the need for temporary local files and keeps your server stateless and scalable.
Using the standard AWS SDK makes interacting with R2 incredibly straightforward, and thanks to Cloudflare's zero egress fee structure, this setup isn't just powerful—it's highly economical for projects of all sizes.
Comments (0)
Login to post a comment.