nestjs-r2
v1.0.4
Published
NestJS module for Cloudflare R2 storage integration with S3-compatible API
Maintainers
Readme
Nestjs R2
NestJS module for integration with Cloudflare R2 storage
A NestJS module for seamless integration with Cloudflare R2 storage, providing S3-compatible file operations with TypeScript support.
Table of Contents
- Introduction
- Features
- Installation
- Quick Start
- API Reference
- Environment Variables
- Cloudflare R2 Credentials
- Testing
- Contributing
- License
- Support
Installation
npm install nestjs-r2
# or
yarn add nestjs-r2
# or
pnpm add nestjs-r2Quick Start
1. Register the Module
Synchronous Registration
import { Module } from "@nestjs/common";
import { R2Module } from "nestjs-r2";
@Module({
imports: [
R2Module.register({
accountId: "your-cloudflare-account-id",
accessKeyId: "your-r2-access-key-id",
secretAccessKey: "your-r2-secret-access-key",
bucket: "your-bucket-name",
publicUrl: "https://your-public-domain.com", // optional
}),
],
})
export class AppModule {}Asynchronous Registration
Using Factory Function
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { R2Module } from "nestjs-r2";
@Module({
imports: [
ConfigModule.forRoot(),
R2Module.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
accountId: configService.get("CLOUDFLARE_ACCOUNT_ID"),
accessKeyId: configService.get("R2_ACCESS_KEY_ID"),
secretAccessKey: configService.get("R2_SECRET_ACCESS_KEY"),
bucket: configService.get("R2_BUCKET"),
publicUrl: configService.get("R2_PUBLIC_URL"),
}),
inject: [ConfigService],
}),
],
})
export class AppModule {}Using Configuration Class
import { Injectable, Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { R2Module, R2ModuleOptions, R2OptionsFactory } from "nestjs-r2";
@Injectable()
export class R2ConfigService implements R2OptionsFactory {
constructor(private configService: ConfigService) {}
createR2Options(): R2ModuleOptions {
return {
accountId: this.configService.get("CLOUDFLARE_ACCOUNT_ID"),
accessKeyId: this.configService.get("R2_ACCESS_KEY_ID"),
secretAccessKey: this.configService.get("R2_SECRET_ACCESS_KEY"),
bucket: this.configService.get("R2_BUCKET"),
publicUrl: this.configService.get("R2_PUBLIC_URL"),
};
}
}
@Module({
imports: [
R2Module.registerAsync({
useClass: R2ConfigService,
}),
],
providers: [R2ConfigService],
})
export class AppModule {}Using Existing Service
import { Module } from "@nestjs/common";
import { R2Module } from "nestjs-r2";
import { MyExistingConfigService } from "./my-existing-config.service";
@Module({
imports: [
R2Module.registerAsync({
useExisting: MyExistingConfigService,
}),
],
providers: [MyExistingConfigService],
})
export class AppModule {}2. Use the Service
import { Injectable } from "@nestjs/common";
import { R2Service } from "nestjs-r2";
@Injectable()
export class FileService {
constructor(private readonly r2Service: R2Service) {}
async uploadFile(file: Express.Multer.File) {
const result = await this.r2Service.upload(file);
return {
key: result.key,
url: result.url, // Public URL if configured
};
}
async downloadFile(key: string) {
const stream = await this.r2Service.get(key);
return stream;
}
async deleteFile(key: string) {
const result = await this.r2Service.delete(key);
return result;
}
}3. Complete Controller Example
import {
Controller,
Post,
Get,
Delete,
Param,
UseInterceptors,
UploadedFile,
Res,
} from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";
import { Response } from "express";
import { R2Service } from "nestjs-r2";
@Controller("files")
export class FileController {
constructor(private readonly r2Service: R2Service) {}
@Post("upload")
@UseInterceptors(FileInterceptor("file"))
async uploadFile(@UploadedFile() file: Express.Multer.File) {
const result = await this.r2Service.upload(file);
return {
message: "File uploaded successfully",
key: result.key,
url: result.url,
};
}
@Get(":key")
async downloadFile(@Param("key") key: string, @Res() res: Response) {
try {
// Option 1: Use the streamToResponse helper method (recommended for production)
await this.r2Service.streamToResponse(key, res);
} catch (error) {
res.status(500).json({ error: "Failed to download file" });
}
}
// Option 2: Use pipeToResponse for simpler error handling
@Get("simple/:key")
async downloadFileSimple(@Param("key") key: string, @Res() res: Response) {
try {
await this.r2Service.pipeToResponse(key, res);
} catch (error) {
res.status(500).json({ error: "Failed to download file" });
}
}
// Option 3: Alternative approach using the get method directly
@Get("direct/:key")
async downloadFileDirect(@Param("key") key: string, @Res() res: Response) {
try {
const stream = await this.r2Service.get(key);
stream.pipe(res as unknown as NodeJS.WritableStream);
} catch (error) {
res.status(500).json({ error: "Failed to download file" });
}
}
@Delete(":key")
async deleteFile(@Param("key") key: string) {
const result = await this.r2Service.delete(key);
return {
message: "File deleted successfully",
...result,
};
}
// Upload R2 file to another service (e.g., Cloudinary)
@Post("transfer/:key")
async transferToCloudinary(@Param("key") key: string) {
try {
// Option 1: Get as buffer
const buffer = await this.r2Service.getAsBuffer(key);
// Upload to Cloudinary (example)
// const cloudinaryResult = await cloudinary.uploader.upload_stream(
// { resource_type: "auto" },
// (error, result) => { ... }
// ).end(buffer);
// Option 2: Get as UploadedFile object
const uploadedFile = await this.r2Service.getAsUploadedFile(key);
// Now you can use uploadedFile with any service that accepts UploadedFile
return { message: "File transferred successfully" };
} catch (error) {
throw new InternalServerErrorException("Transfer failed");
}
}
}API Reference
R2Service
upload(file: any): Promise<{ key: string; url: string | null }>
Uploads a file to R2 storage.
Parameters:
file: File object withoriginalname,buffer, andmimetypeproperties
Returns:
key: Generated unique key for the fileurl: Public URL ifpublicUrlis configured, otherwisenull
get(key: string): Promise<Readable>
Downloads a file from R2 storage and returns a Node.js Readable stream.
Parameters:
key: The file key
Returns:
Readable: A Node.js Readable stream that can be piped to a response
Note: This method will never return undefined. If the file is not found, it will throw a NotFoundException.
streamToResponse(key: string, response: any): Promise<void>
Downloads a file from R2 storage and pipes it directly to a response object with proper error handling.
Parameters:
key: The file keyresponse: The response object (Express or Fastify response)
Returns:
Promise<void>: Resolves when the stream is fully piped to the response
Recommended Usage: This method handles all the stream piping and error handling for you.
pipeToResponse(key: string, response: any): Promise<void>
Downloads a file from R2 storage and pipes it directly to a response object with basic error handling and completion tracking.
Parameters:
key: The file keyresponse: The response object (Express or Fastify response)
Returns:
Promise<void>: Resolves when the file is fully piped to the response
Usage: This is a simpler alternative to streamToResponse with basic error handling. Use this when you need straightforward file serving with completion tracking but don't require comprehensive error handling.
Difference from streamToResponse:
streamToResponse: Full error handling for both stream and response errors (recommended for production)pipeToResponse: Basic stream error handling, simpler implementation, still tracks completion
getAsBuffer(key: string): Promise<Buffer>
Downloads a file from R2 storage and returns it as a Buffer. Perfect for uploading to other services.
Parameters:
key: The file key
Returns:
Buffer: The file content as a Buffer that can be used with other APIs
Example Usage:
// Get file as buffer for uploading to Cloudinary
const buffer = await this.r2Service.getAsBuffer("my-file-key");
// Upload to Cloudinary
const cloudinaryResult = await cloudinary.uploader
.upload_stream({ resource_type: "auto" }, (error, result) => {
if (error) throw error;
console.log("Uploaded to Cloudinary:", result.secure_url);
})
.end(buffer);getAsUploadedFile(key: string, originalName?: string): Promise<UploadedFile>
Downloads a file from R2 storage and returns it as an UploadedFile object compatible with other upload services.
Parameters:
key: The file keyoriginalName: Optional original filename (defaults to the key)
Returns:
UploadedFile: A file object that can be used with other upload methods
Example Usage:
// Get file as UploadedFile object
const uploadedFile = await this.r2Service.getAsUploadedFile(
"my-file-key",
"photo.jpg"
);
// Now you can pass this to any service that accepts UploadedFile
await someOtherUploadService.upload(uploadedFile);getMetadata(key: string): Promise<{ contentType?: string; contentLength?: number; lastModified?: Date }>
Gets file metadata without downloading the full file content.
Parameters:
key: The file key
Returns:
contentType: The MIME type of the filecontentLength: The size of the file in byteslastModified: When the file was last modified
delete(key: string): Promise<{ deleted: boolean }>
Deletes a file from R2 storage.
Readable: Node.js readable stream
delete(key: string): Promise<{ deleted: boolean }>
Deletes a file from R2 storage.
Parameters:
key: The file key
Returns:
deleted: Boolean indicating success
Configuration Options
R2ModuleOptions
interface R2ModuleOptions {
accountId: string; // Cloudflare Account ID
accessKeyId: string; // R2 Access Key ID
secretAccessKey: string; // R2 Secret Access Key
bucket: string; // R2 Bucket name
publicUrl?: string; // Optional public URL for file access
}R2ModuleAsyncOptions
interface R2ModuleAsyncOptions {
imports?: any[]; // Optional modules to import
useFactory?: (...args: any[]) => Promise<R2ModuleOptions> | R2ModuleOptions;
useClass?: Type<R2OptionsFactory>;
useExisting?: Type<R2OptionsFactory>;
inject?: any[]; // Dependencies to inject (used with useFactory)
}R2OptionsFactory
interface R2OptionsFactory {
createR2Options(): Promise<R2ModuleOptions> | R2ModuleOptions;
}Environment Variables
Create a .env file in your project root:
CLOUDFLARE_ACCOUNT_ID=your-account-id
R2_ACCESS_KEY_ID=your-access-key-id
R2_SECRET_ACCESS_KEY=your-secret-access-key
R2_BUCKET=your-bucket-name
R2_PUBLIC_URL=https://your-public-domain.comGetting Cloudflare R2 Credentials
- Sign up for Cloudflare and navigate to R2 Object Storage
- Create an R2 bucket in your desired region
- Generate API tokens:
- Go to "Manage R2 API tokens"
- Click "Create API token"
- Select permissions for your bucket
- Note down the Access Key ID and Secret Access Key
- Find your Account ID in the right sidebar of the Cloudflare dashboard
- Set up custom domain (optional) for public file access
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
Made with ❤️ by Shejan Mahamud
