New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

nestjs-r2-storage

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

nestjs-r2-storage

Production-ready NestJS module for Cloudflare R2 object storage management

latest
Source
npmnpm
Version
1.6.0
Version published
Weekly downloads
2
-71.43%
Maintainers
1
Weekly downloads
 
Created
Source

nestjs-r2-storage

npm version License: MIT

Author: Nurul Islam Rimon
GitHub: https://github.com/nurulislamrimon/nestjs-r2-storage

Nestjs R2 Storage

Production-ready NestJS module for Cloudflare R2 object storage management.

Features

  • Signed Upload URLs - Generate presigned URLs for direct file uploads
  • Signed Download URLs - Generate presigned URLs for secure file downloads
  • File Deletion - Delete files from R2 storage
  • Nested Field Support - Handle paths like shop.logo, profile.avatar
  • Array Field Support - Handle paths like products[].image, gallery[].photo
  • Nested Array Paths - Handle paths like order_items[].product.photo_1
  • Storage Usage Tracking - Track storage used, increased, and decreased
  • Full CRUD Lifecycle - Create, Update, Delete file operations
  • Access Control Modes - Control public vs signed URL access (private, public-read, hybrid)

Access Control Modes

Cloudflare R2 does NOT enforce ACLs like AWS S3 - the R2 API ignores ACL headers. True security is achieved by controlling URL exposure.

Modes

ModePublic URLsSigned URLsUse Case
privateNot allowedRequiredMaximum security - only signed access
public-readAllowedOptionalPublic files (e.g., static assets)
hybridAllowedAllowedMixed content (default)

Private Mode

Only presigned URLs are allowed. Public URL generation throws AccessModeError.

R2StorageModule.forRoot({
  // ... other options
  accessMode: "private",
  publicUrlBase: "https://cdn.example.com", // still configured but not used
});

Response in private mode:

{
  "uploadUrl": "https://signed-url...",
  "publicUrl": null
}

Public-Read Mode

Public URLs are generated. Signed URLs are optional.

R2StorageModule.forRoot({
  // ... other options
  accessMode: "public-read",
  publicUrlBase: "https://cdn.example.com",
});

Hybrid Mode (Default)

Both public and signed access are allowed for backward compatibility.

R2StorageModule.forRoot({
  // ... other options
  accessMode: "hybrid", // default
});

Quick Start

1. Configure the Module

// app.module.ts
import { Module } from "@nestjs/common";
import { R2StorageModule } from "nestjs-r2-storage";

@Module({
  imports: [
    R2StorageModule.forRoot({
      endpoint: process.env.R2_ENDPOINT,
      accessKeyId: process.env.R2_ACCESS_KEY,
      secretAccessKey: process.env.R2_SECRET_KEY,
      bucketName: process.env.R2_BUCKET,
      region: "auto",
      publicUrlBase: `https://${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${process.env.R2_BUCKET}`,
      signedUrlExpiry: 3600,
    }),
  ],
})
export class AppModule {}

2. Use in Your Service

import { Injectable } from "@nestjs/common";
import {
  PhotoManagerService,
  PhotoField,
  CloudflareService,
} from "nestjs-r2-storage";

@Injectable()
export class ProductService {
  constructor(
    private readonly photoManager: PhotoManagerService,
    private readonly cloudflare: CloudflareService,
  ) {}

  async createProduct(payload: any) {
    const photoFields: PhotoField[] = [
      { field: "image", urlField: "image_url", sizeField: "image_size" },
      {
        field: "gallery[].photo",
        urlField: "photo_url",
        sizeField: "photo_size",
      },
    ];

    const result = await this.photoManager.createObjectWithPhotos(
      payload,
      photoFields,
    );

    // Return upload URLs to client for direct upload
    return {
      product: result.updatedPayload,
      uploadUrls: result.uploadUrls,
      totalStorageUsed: result.totalStorageUsed,
    };
  }

  async getProduct(id: string) {
    const product = await this.findProduct(id);

    const photoFields: PhotoField[] = [
      { field: "image", urlField: "image_url" },
      { field: "gallery[].photo", urlField: "photo_url" },
    ];

    return this.photoManager.appendPhotoUrls(product, photoFields);
  }

  async updateProduct(id: string, payload: any) {
    const existing = await this.findProduct(id);

    const photoFields: PhotoField[] = [
      { field: "image", urlField: "image_url", sizeField: "image_size" },
    ];

    const result = await this.photoManager.updateObjectWithPhotos(
      payload,
      existing,
      photoFields,
    );

    return {
      product: result.updatedPayload,
      uploadUrls: result.uploadUrls,
      storageIncrease: result.storageIncrease,
      storageDecrease: result.storageDecrease,
    };
  }

  async deleteProduct(id: string) {
    const product = await this.findProduct(id);

    const photoFields: PhotoField[] = [
      { field: "image", urlField: "image_url" },
    ];

    await this.photoManager.deletePhotosFromObject(product, photoFields);
    await this.removeProduct(id);
  }
}

Nested Array Paths

The package supports accessing nested properties within array items using paths like order_items[].product.photo_1.

Example: E-commerce Order with Product Photos

const order = {
  id: "order_123",
  order_items: [
    {
      product: {
        id: "prod_1",
        name: "Laptop",
        photo_1: "laptop.png",
        photo_1_size: 50000,
      },
    },
    {
      product: {
        id: "prod_2",
        name: "Mouse",
        photo_1: "mouse.png",
        photo_1_size: 10000,
      },
    },
  ],
};

const photoFields: PhotoField[] = [
  {
    field: "order_items[].product.photo_1",
    sizeField: "order_items[].product.photo_1_size",
    urlField: "order_items[].product.photo_1_url",
  },
];

// Generate signed URLs for all product photos
const result = await photoManager.appendPhotoUrls(order, photoFields);

// Result:
// {
//   id: "order_123",
//   order_items: [
//     {
//       product: {
//         id: "prod_1",
//         name: "Laptop",
//         photo_1: "laptop.png",
//         photo_1_size: 50000,
//         photo_1_url: "https://signed-url-for-laptop..."
//       }
//     },
//     {
//       product: {
//         id: "prod_2",
//         name: "Mouse",
//         photo_1: "mouse.png",
//         photo_1_size: 10000,
//         photo_1_url: "https://signed-url-for-mouse..."
//       }
//     }
//   ]
// }

API Reference

CloudflareService

Direct R2 operations.

// Generate upload URL
const uploadUrl = await cloudflare.getUploadUrl("avatar.png", 1024000);

// Generate download URL
const downloadUrl = await cloudflare.getDownloadUrl("uploads/avatar_123.png");

// Delete file
await cloudflare.deleteFile("uploads/avatar.png");

// Check if file exists
const exists = await cloudflare.fileExists("uploads/avatar.png");

Presigned URL Security

The module uses secure presigned URL generation:

  • Content-Length is NOT signed - Prevents SignatureDoesNotMatch errors (browsers calculate it differently)
  • Checksum headers disabled - Uses requestChecksumCalculation: "WHEN_REQUIRED" to avoid R2 compatibility issues
  • Minimal signing - Only signs host and content-type headers
const result = await cloudflare.getUploadUrl("avatar.png", 1024000);

// result = {
//   uploadUrl: "https://signed-url...",
//   fileKey: "uploads/avatar_123.png",
//   publicUrl: "https://cdn.example.com/uploads/avatar_123.png",
//   mimeType: "image/png",
//   sizeField: 1024000  // Use this for client-side validation before upload
// }

PhotoManagerService

High-level photo management.

appendPhotoUrls()

Adds signed URLs to response objects.

const photoFields: PhotoField[] = [
  { field: "avatar", urlField: "avatar_url" },
  { field: "shop.logo", urlField: "logo_url" },
  { field: "products[].image", urlField: "image_url" },
  { field: "gallery[].photo", urlField: "photo_url" },
  { field: "order_items[].product.photo_1", urlField: "photo_1_url" },
];

const result = await photoManager.appendPhotoUrls(product, photoFields);

Input:

{
  "name": "Laptop",
  "image": "laptop.png",
  "gallery": [{ "photo": "photo1.jpg" }, { "photo": "photo2.jpg" }]
}

Output:

{
  "name": "Laptop",
  "image": "laptop.png",
  "image_url": "https://signed-url...",
  "gallery": [
    { "photo": "photo1.jpg", "photo_url": "https://signed-url..." },
    { "photo": "photo2.jpg", "photo_url": "https://signed-url..." }
  ]
}

createObjectWithPhotos()

Creates object with photo upload URLs.

const payload = {
  name: "Laptop",
  image: "laptop.png",
  image_size: 42000,
  gallery: [
    { photo: "photo1.jpg", photo_size: 10000 },
    { photo: "photo2.jpg", photo_size: 15000 },
  ],
};

const photoFields: PhotoField[] = [
  { field: "image", sizeField: "image_size" },
  { field: "gallery[].photo", sizeField: "gallery[].photo_size" },
];

const result = await photoManager.createObjectWithPhotos(payload, photoFields);

// result = {
//   updatedPayload: { ...with generated file keys... },
//   uploadUrls: [{ field, fileKey, uploadUrl, publicUrl }],
//   totalStorageUsed: 67000
// }

updateObjectWithPhotos()

Updates object with new photos, deletes old files.

const result = await photoManager.updateObjectWithPhotos(
  newPayload,
  existingObject,
  photoFields,
);

// result = {
//   updatedPayload: { ... },
//   uploadUrls: [{ field, fileKey, uploadUrl, publicUrl }],
//   storageIncrease: 1000,
//   storageDecrease: 500,
//   deletedFiles: ['old-file.png']
// }

deletePhotosFromObject()

Deletes all photos from object.

const result = await photoManager.deletePhotosFromObject(product, photoFields);

// result = {
//   deletedFiles: ['file1.png', 'file2.jpg'],
//   totalStorageFreed: 25000
// }

Field Path Syntax

Simple Nested Fields

shop.logo
profile.avatar
user.profile.image

Array Fields

gallery[].photo        -> gallery[0].photo, gallery[1].photo, ...
products[].image      -> products[0].image, products[1].image, ...
variants[].images[]   -> variants[0].images[0], variants[0].images[1], ...

Nested Array Paths

order_items[].product.photo_1        -> Access photo_1 inside product inside each order item
users[].profile.avatar               -> Access avatar inside profile inside each user
categories[].items[].image           -> Deeply nested arrays with properties

Supported Patterns

PathDescription
shop.logoSimple nested field
user.profile.imageDeeply nested with dots
gallery[].photoArray of objects
products[].images[]Array containing array
variants[0].images[].urlIndexed array with nested array
order_items[].product.photo_1Nested property in array items

Configuration Options

OptionTypeRequiredDescription
endpointstringYesR2 endpoint URL
accessKeyIdstringYesR2 access key ID
secretAccessKeystringYesR2 secret access key
bucketNamestringYesR2 bucket name
regionstringNoAWS region (default: 'auto')
publicUrlBasestringNoBase URL for public access
signedUrlExpirynumberNoSigned URL expiry in seconds (default: 3600)
accessModestringNoAccess mode: private, public-read, hybrid (default: hybrid)

Error Handling

AccessModeError

Thrown when attempting to generate public URLs in private access mode.

import { AccessModeError } from "nestjs-r2-storage";

try {
  const result = await cloudflare.getUploadUrl("file.png", 1024);
} catch (error) {
  if (error instanceof AccessModeError) {
    console.log(error.message); // "Public URL generation is not allowed in 'private' access mode..."
  }
}

Async Configuration

R2StorageModule.forRootAsync({
  useFactory: () => ({
    endpoint: process.env.R2_ENDPOINT,
    accessKeyId: process.env.R2_ACCESS_KEY,
    secretAccessKey: process.env.R2_SECRET_KEY,
    bucketName: process.env.R2_BUCKET,
  }),
});

Changelog

v1.6.0 (2026-05-03)

  • Nested array path support - Handle paths like order_items[].product.photo_1
  • Improved path parsing - Better handling of nested properties after array notation
  • New utility function - Added getSubPathAfterArray() to extract sub-paths after array segments
  • Updated exports - Replaced getArrayBasePath and getArrayElementPath with getSubPathAfterArray

v1.5.0 (2026-04-25)

  • Refactored photo update logic - Diff-based (state reconciliation) instead of request-driven
  • No unnecessary uploads - Files are only uploaded when the value actually changes
  • No accidental deletions - Files are only deleted when removed from the payload
  • Index-based array handling - Array fields use path-based comparison (gallery[0].photo) instead of filename matching
  • Reusable extractExistingFileMap() - Public method for extracting fieldPath → fileKey maps
  • Optimized traversal - Single-pass map extraction, O(1) lookups via Map
  • Size is optional - Size field does not affect diff logic, only used for storage tracking
  • Deterministic behavior - Backend-driven, no assumptions about frontend behavior

v1.2.6 (2025-04-20)

  • Refactored getNestedValue: access key first, then handle array segments
  • Refactored setNestedValue: proper handling of empty brackets [] and indexed arrays [0]
  • Robust parsing of paths: user.profile.image, gallery[].photo, variants[0].images[].url

v1.2.5 (2025-04-20)

  • Rewrote parseFieldPath to split by dot then parse each segment (fixes regex state bugs)
  • Fixed getNestedValue array traversal when next key is a property (not array/index)
  • Fixed setNestedValue for empty array brackets [] and indexed arrays [0]
  • Added null/undefined guards throughout
  • Supports: gallery[].photo, variants[].images[].url, a[].b[0].c

v1.2.4 (2025-04-20)

  • Fixed parseFieldPath regex to handle keys containing dots
  • Fixed parseFieldPath empty bracket handling ([] now correctly returns undefined for arrayIndex)
  • Fixed getNestedValue array traversal for paths like variants[].photo
  • Added null/undefined guards in array field processing methods
  • Improved safety for deeply nested array structures

v1.2.3 (2025-04-13)

  • Added AccessModeError for private mode public URL generation

License

MIT

Keywords

nestjs

FAQs

Package last updated on 03 May 2026

Related posts