Skip to content

Cloudflare R2 Driver ​

The R2 driver provides integration with Cloudflare R2, Cloudflare's S3-compatible object storage with zero egress fees. The driver extends S3Disk, so all S3 features (multipart uploads, presigned URLs, range requests, conditional writes) are available.

Configuration ​

FieldTypeRequiredDefaultDescription
driver'r2'Yes—Must be 'r2'
bucketstringYes—R2 bucket name
accountIdstringYes—Cloudflare account ID
keystringYes—R2 API token access key ID
secretstringYes—R2 API token secret access key
regionstringNo'auto'R2 region hint (usually 'auto')
urlstringNo—Custom domain base URL for public access
visibility'public' | 'private'No'private'Default visibility

Basic Setup ​

typescript
import { Module } from '@nestjs/common';
import { StorageModule } from '@fozooni/nestjs-storage';

@Module({
  imports: [
    StorageModule.forRoot({
      default: 'r2',
      disks: {
        r2: {
          driver: 'r2',
          bucket: 'my-app-assets',
          accountId: process.env.CF_ACCOUNT_ID,
          key: process.env.R2_ACCESS_KEY_ID,
          secret: process.env.R2_SECRET_ACCESS_KEY,
          url: 'https://assets.example.com', // Custom domain
        },
      },
    }),
  ],
})
export class AppModule {}

Async Configuration with ConfigService ​

typescript
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { StorageModule } from '@fozooni/nestjs-storage';

@Module({
  imports: [
    ConfigModule.forRoot(),
    StorageModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        default: 'r2',
        disks: {
          r2: {
            driver: 'r2',
            bucket: config.getOrThrow('R2_BUCKET'),
            accountId: config.getOrThrow('CF_ACCOUNT_ID'),
            key: config.getOrThrow('R2_ACCESS_KEY_ID'),
            secret: config.getOrThrow('R2_SECRET_ACCESS_KEY'),
            url: config.get('R2_CUSTOM_DOMAIN'),
          },
        },
      }),
    }),
  ],
})
export class AppModule {}

R2 API Token Setup ​

To generate R2 API credentials:

  1. Log into the Cloudflare dashboard
  2. Navigate to R2 in the sidebar
  3. Click Manage R2 API Tokens
  4. Click Create API Token
  5. Select permissions:
    • Object Read & Write for full access
    • Object Read for read-only access
  6. Optionally restrict to specific buckets
  7. Copy the Access Key ID and Secret Access Key

Your accountId is visible in the Cloudflare dashboard URL: https://dash.cloudflare.com/<accountId>/r2

TIP

The R2 endpoint is automatically constructed from your account ID:

https://<accountId>.r2.cloudflarestorage.com

You do not need to set the endpoint field manually.

R2 Extends S3Disk ​

Under the hood, R2Disk extends S3Disk and configures the S3 client to point at the R2 endpoint:

S3Disk
└── R2Disk (endpoint: https://<accountId>.r2.cloudflarestorage.com)

This means every feature documented on the S3 driver page works identically with R2:

  • Presigned URLs via temporaryUrl() and temporaryUploadUrl()
  • Presigned POST via presignedPost()
  • Multipart uploads via initMultipartUpload() / putFileMultipart()
  • Range requests via getRange()
  • Conditional writes via putIfMatch() / putIfNoneMatch()
  • Streaming via getStream() / putStream()
  • Checksums and custom metadata

No Egress Fees ​

R2's primary advantage over S3 is zero egress fees. Data transfer out is free regardless of volume. This makes R2 particularly cost-effective for:

  • Public asset hosting (images, videos, documents)
  • Frequently downloaded files
  • API responses serving stored content
  • CDN origin storage

R2 vs S3 Differences

While R2 is S3-compatible, there are some differences to be aware of:

FeatureS3R2
Egress feesPer-GB chargesFree
Per-object ACLsSupportedNot supported
Storage classesMultiple (Standard, IA, Glacier, etc.)Single class
RegionsChoose specific regionAutomatic (globally distributed)
VersioningSupportedSupported
Lifecycle rulesFull supportBasic support
S3 SelectSupportedNot supported
ReplicationCross-region replicationNot supported

Public Access and Custom Domains ​

R2 does not support per-object ACLs. Instead, public access is controlled at the bucket level through Cloudflare's dashboard:

  1. Go to R2 > your bucket > Settings
  2. Under Public access, click Allow Access
  3. Optionally connect a Custom Domain
typescript
// With a custom domain configured:
const disk = this.storage.disk('r2');

// Returns: https://assets.example.com/images/hero.jpg
const url = await disk.url('images/hero.jpg');

No Per-Object ACL Support

Unlike S3, R2 does not support per-object ACLs. The visibility config option controls the default behavior, but you cannot make individual objects public or private. Public access must be enabled or disabled at the bucket level through the Cloudflare dashboard.

typescript
// This will NOT make the individual object public on R2
await disk.put('file.txt', content, { visibility: 'public' });
// The file's accessibility depends on the bucket-level public access setting

Presigned URLs ​

Even without public bucket access, you can generate presigned URLs for temporary access:

typescript
@Injectable()
export class R2DownloadService {
  constructor(private readonly storage: StorageService) {}

  async getTemporaryLink(path: string): Promise<string> {
    const disk = this.storage.disk('r2');

    // Generate a presigned URL valid for 1 hour
    const url = await disk.temporaryUrl(path, 3600);

    return url;
  }
}

Multipart Uploads ​

typescript
@Injectable()
export class R2UploadService {
  constructor(private readonly storage: StorageService) {}

  async uploadLargeFile(path: string, stream: Readable) {
    const disk = this.storage.disk('r2');

    // Convenience method handles chunking automatically
    await disk.putFileMultipart(path, stream, {
      partSize: 10 * 1024 * 1024, // 10 MB parts
      concurrency: 3,
    });
  }

  async uploadWithPresignedUrl(path: string) {
    const disk = this.storage.disk('r2');

    // Generate a presigned URL for client-side upload
    const url = await disk.temporaryUploadUrl(path, 3600, {
      contentType: 'application/pdf',
    });

    return { uploadUrl: url };
  }
}

Range Requests and Streaming ​

typescript
@Controller('assets')
export class AssetController {
  constructor(private readonly storage: StorageService) {}

  @Get(':path(*)')
  @RangeServe()
  async serve(@Param('path') path: string) {
    return { disk: 'r2', path };
  }
}

Using R2 with Cloudflare Workers ​

If you deploy your NestJS application alongside Cloudflare Workers, R2 can be accessed with lower latency from Workers. For server-side NestJS applications, the driver communicates with R2 via the S3-compatible HTTP API.

typescript
// Standard NestJS usage — communicates via HTTPS
const disk = this.storage.disk('r2');
await disk.put('data/report.json', JSON.stringify(report));

Complete Example: Asset Service ​

typescript
import { Injectable, NotFoundException } from '@nestjs/common';
import { StorageService } from '@fozooni/nestjs-storage';
import { v4 as uuid } from 'uuid';

@Injectable()
export class AssetService {
  constructor(private readonly storage: StorageService) {}

  private get disk() {
    return this.storage.disk('r2');
  }

  async upload(file: Express.Multer.File): Promise<{ key: string; url: string }> {
    const ext = file.originalname.split('.').pop();
    const key = `assets/${uuid()}.${ext}`;

    await this.disk.put(key, file.buffer, {
      metadata: {
        originalName: file.originalname,
        mimeType: file.mimetype,
      },
    });

    return {
      key,
      url: await this.disk.url(key),
    };
  }

  async getTemporaryUrl(key: string, expiresIn = 3600): Promise<string> {
    if (!(await this.disk.exists(key))) {
      throw new NotFoundException('Asset not found');
    }

    return this.disk.temporaryUrl(key, expiresIn);
  }

  async delete(key: string): Promise<void> {
    await this.disk.delete(key);
  }

  async listAssets(prefix = 'assets/') {
    return this.disk.listContents(prefix);
  }
}

Environment Variables Example ​

bash
# .env
CF_ACCOUNT_ID=your-cloudflare-account-id
R2_BUCKET=my-app-assets
R2_ACCESS_KEY_ID=your-r2-access-key-id
R2_SECRET_ACCESS_KEY=your-r2-secret-access-key
R2_CUSTOM_DOMAIN=https://assets.example.com

Released under the MIT License.