
Security News
arXiv Is Rate Limiting Authors Following a Flood of AI Slop Submissions
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.
nestjs-r2-storage
Advanced tools
Production-ready NestJS module for Cloudflare R2 object storage management
Author: Nurul Islam Rimon
GitHub: https://github.com/nurulislamrimon/nestjs-r2-storage

Production-ready NestJS module for Cloudflare R2 object storage management.
shop.logo, profile.avatarproducts[].image, gallery[].photoorder_items[].product.photo_1private, public-read, hybrid)Cloudflare R2 does NOT enforce ACLs like AWS S3 - the R2 API ignores ACL headers. True security is achieved by controlling URL exposure.
| Mode | Public URLs | Signed URLs | Use Case |
|---|---|---|---|
private | Not allowed | Required | Maximum security - only signed access |
public-read | Allowed | Optional | Public files (e.g., static assets) |
hybrid | Allowed | Allowed | Mixed content (default) |
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 URLs are generated. Signed URLs are optional.
R2StorageModule.forRoot({
// ... other options
accessMode: "public-read",
publicUrlBase: "https://cdn.example.com",
});
Both public and signed access are allowed for backward compatibility.
R2StorageModule.forRoot({
// ... other options
accessMode: "hybrid", // default
});
// 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 {}
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);
}
}
The package supports accessing nested properties within array items using paths like order_items[].product.photo_1.
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..."
// }
// }
// ]
// }
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");
The module uses secure presigned URL generation:
SignatureDoesNotMatch errors (browsers calculate it differently)requestChecksumCalculation: "WHEN_REQUIRED" to avoid R2 compatibility issueshost and content-type headersconst 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
// }
High-level photo management.
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..." }
]
}
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
// }
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']
// }
Deletes all photos from object.
const result = await photoManager.deletePhotosFromObject(product, photoFields);
// result = {
// deletedFiles: ['file1.png', 'file2.jpg'],
// totalStorageFreed: 25000
// }
shop.logo
profile.avatar
user.profile.image
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], ...
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
| Path | Description |
|---|---|
shop.logo | Simple nested field |
user.profile.image | Deeply nested with dots |
gallery[].photo | Array of objects |
products[].images[] | Array containing array |
variants[0].images[].url | Indexed array with nested array |
order_items[].product.photo_1 | Nested property in array items |
| Option | Type | Required | Description |
|---|---|---|---|
endpoint | string | Yes | R2 endpoint URL |
accessKeyId | string | Yes | R2 access key ID |
secretAccessKey | string | Yes | R2 secret access key |
bucketName | string | Yes | R2 bucket name |
region | string | No | AWS region (default: 'auto') |
publicUrlBase | string | No | Base URL for public access |
signedUrlExpiry | number | No | Signed URL expiry in seconds (default: 3600) |
accessMode | string | No | Access mode: private, public-read, hybrid (default: hybrid) |
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..."
}
}
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,
}),
});
order_items[].product.photo_1getSubPathAfterArray() to extract sub-paths after array segmentsgetArrayBasePath and getArrayElementPath with getSubPathAfterArraygallery[0].photo) instead of filename matchingextractExistingFileMap() - Public method for extracting fieldPath → fileKey maps[] and indexed arrays [0]gallery[].photo, variants[].images[].url, a[].b[0].c[] now correctly returns undefined for arrayIndex)variants[].photoMIT
FAQs
Production-ready NestJS module for Cloudflare R2 object storage management
The npm package nestjs-r2-storage receives a total of 2 weekly downloads. As such, nestjs-r2-storage popularity was classified as not popular.
We found that nestjs-r2-storage demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
arXiv now limits authors to two submissions a month as AI slop overwhelms moderators, delays good papers, and sparks debate over applying the limit to everyone.

Research
/Security News
A new GhostAction wave hits hundreds of GitHub repos, expanding CI/CD secret theft to cloud and AI credentials in source code and git history.

Research
/Security News
Tensorlake npm SDK version 0.5.144 was compromised in a ChainDrop / Shai-Hulud attack, delivering credential-stealing malware.