S3-compatible client wrapper for Cloudflare R2, supporting both ESM and CommonJS.
- Interacting with object storage APIs, especially Cloudflare R2, should be simple and straightforward.
- No official Node.js library exists for Cloudflare R2; the AWS SDK works but requires boilerplate.
npm install node-cloudflare-r2
# or
pnpm install node-cloudflare-r2It is recommended to pin a specific version to avoid unexpected breaking changes. Check the release page for the latest version.
The package supports both import (ESM) and require (CommonJS).
// ESM
import { R2 } from 'node-cloudflare-r2';
// CommonJS
const { R2 } = require('node-cloudflare-r2');For TypeScript, types are available at the top level and via the types subpath:
import type { CloudflareR2Config, UploadStreamOptions } from 'node-cloudflare-r2/types';const r2 = new R2({
accountId: '<YOUR_ACCOUNT_ID>',
accessKeyId: '<YOUR_R2_ACCESS_KEY_ID>',
secretAccessKey: '<YOUR_R2_SECRET_ACCESS_KEY>',
});
const bucket = r2.bucket('<BUCKET_NAME>');
// Provide the public URL(s) of your bucket (optional, if public access is enabled)
bucket.provideBucketPublicUrl('https://pub-xxxxxxxxxxxxxxxxxxxxxxxxx.r2.dev');
console.log(await bucket.exists()); // trueconst upload = await bucket.uploadFile('/path/to/file', 'destination_file_name.ext');
console.log(upload);
/*
{
objectKey: 'destination_file_name.ext',
uri: 'https://<accountId>.r2.cloudflarestorage.com/<bucket>/destination_file_name.ext',
publicUrl: 'https://pub-xxxxxxxxxxxxxxxxxxxxxxxxx.r2.dev/destination_file_name.ext',
publicUrls: ['https://pub-xxxxxxxxxxxxxxxxxxxxxxxxx.r2.dev/destination_file_name.ext'],
etag: '',
versionId: '',
}
*/// Text content
await bucket.upload('Lorem ipsum', 'lorem-ipsum.txt');
// From a readable stream
import { createReadStream } from 'fs';
const stream = createReadStream('/path/to/file');
await bucket.upload(stream, 'destination_file_name2.ext');Use uploadStream() for large files or live streams. It uses multipart upload internally and accepts tuning options to avoid the 10,000 part limit.
import { spawn } from 'child_process';
const streamlink = spawn('streamlink', ['--stdout', '<LIVE_STREAM_HLS_URL>', 'best']);
// With default multipart settings
await bucket.uploadStream(streamlink.stdout, 'my_live_stream.ts');
// With custom part size and concurrency (for very large files)
await bucket.uploadStream(largeBuffer, 'bigfile.bin', undefined, undefined, undefined, {
partSize: 50 * 1024 * 1024, // 50 MB per part
queueSize: 8, // 8 concurrent uploads
});// GET signed URL (expires after 3600 seconds)
const getUrl = await bucket.getObjectSignedUrl('file.ext', 3600);
// PUT signed URL (expires after 3600 seconds)
const putUrl = await bucket.putObjectSignedUrl('file.ext', 3600);Creates a new R2 client instance.
const r2 = new R2({
accountId: '<YOUR_ACCOUNT_ID>',
accessKeyId: '<YOUR_R2_ACCESS_KEY_ID>',
secretAccessKey: '<YOUR_R2_SECRET_ACCESS_KEY>',
jurisdiction: 'eu', // optional: 'eu' or 'fedramp'
});You can also pass S3 client overrides as a second argument:
const r2 = new R2(config, { endpoint: 'https://custom.example.com' });Returns a Bucket instance for the given bucket name.
const bucket = r2.bucket('my-bucket');Returns all buckets owned by the authenticated account.
const { buckets, owner } = await r2.listBuckets();
// buckets: [{ name, creationDate }, ...]
// owner: { id, displayName }Returns true if the bucket exists and you have permission to access it.
if (await r2.bucketExists('my-bucket')) { ... }Creates a new bucket and returns a Bucket instance. Optionally specify a location hint (e.g. 'WNAM', 'WEUR', 'APAC').
const bucket = await r2.createBucket('new-bucket');
const euBucket = await r2.createBucket('new-bucket-eu', 'WEUR');Deletes a bucket. Returns true on success, throws on failure.
await r2.deleteBucket('old-bucket');Returns the CORS configuration for a bucket. Shortcut for r2.bucket(name).getCorsPolicies().
const policies = await r2.getBucketCors('my-bucket');Returns the region of a bucket. For Cloudflare R2, this is the bucket's location hint (e.g. 'WNAM', 'WEUR', 'APAC') if one was set, otherwise 'auto'.
const region = await r2.getBucketRegion('my-bucket'); // 'APAC'Read-only properties for the bucket name and its R2 endpoint URI.
Returns true if the bucket exists and is accessible.
console.log(await bucket.exists()); // trueUploads a string, Buffer, Uint8Array, or Readable stream to the bucket.
await bucket.upload('Hello world', 'hello.txt');
await bucket.upload(buffer, 'data.bin', { author: 'me' }, 'application/octet-stream');Uploads a local file from disk. If destination is omitted, the file's basename is used.
await bucket.uploadFile('/path/to/photo.jpg');
await bucket.uploadFile('/path/to/video.mp4', 'videos/intro.mp4');Uploads using multipart upload internally. Best for large files or live streams. Use the options parameter to tune part size and concurrency.
// Basic
await bucket.uploadStream(largeBuffer, 'bigfile.bin');
// With progress tracking
await bucket.uploadStream(stream, 'bigfile.bin', fileMetadata, fileMimeType, (progress) => {
console.log(`${progress.loaded} / ${progress.total} bytes`);
});
// With custom part size (avoid 10,000 part limit for very large files)
await bucket.uploadStream(stream, 'huge.bin', fileMetadata, fileMimeType, onProgress, {
partSize: 50 * 1024 * 1024, // 50 MB per part
queueSize: 8, // 8 concurrent uploads
});All upload methods return an UploadFileResponse:
{ objectKey, uri, publicUrl, publicUrls, etag?, versionId? }Deletes an object from the bucket.
await bucket.deleteObject('old-file.txt');Returns metadata for an object without downloading it.
const meta = await bucket.headObject('file.txt');
// { lastModified, contentLength, acceptRanges, etag, contentType, customMetadata }Lists up to 1,000 objects in the bucket. Use marker for pagination.
const page1 = await bucket.listObjects(100);
// { objects, continuationToken, nextContinuationToken }
// Next page
const page2 = await bucket.listObjects(100, page1.nextContinuationToken);Each object in the list: { key, lastModified, etag, checksumAlgorithm, size, storageClass }.
Copies an object within the same bucket.
await bucket.copyObject('source.txt', 'backup/copy.txt');Returns true if the object exists in the bucket.
if (await bucket.objectExists('file.txt')) { ... }Returns all public URLs for an object (requires provideBucketPublicUrl to be set first).
bucket.provideBucketPublicUrl('https://pub-xxxx.r2.dev');
console.log(bucket.getObjectPublicUrls('photo.jpg'));
// ['https://pub-xxxx.r2.dev/photo.jpg']Generates pre-signed URLs for GET or PUT operations, valid for expiresIn seconds.
const getUrl = await bucket.getObjectSignedUrl('file.pdf', 3600);
const putUrl = await bucket.putObjectSignedUrl('uploads/file.pdf', 7200);Returns the CORS configuration of the bucket.
const policies = await bucket.getCorsPolicies();
// [{ allowedHeaders, allowedMethods, allowedOrigins, exposeHeaders, id, maxAgeSeconds }]Returns the bucket's location constraint. For Cloudflare R2, this is the bucket's location hint (e.g. 'WNAM', 'WEUR', 'APAC') if one was set, otherwise 'auto'.
console.log(await bucket.getRegion()); // 'APAC'Returns the server-side encryption configuration of the bucket.
console.log(await bucket.getEncryption()); // [{ applyServerSideEncryptionByDefault, bucketKeyEnabled }]Sets and retrieves the public-facing URL(s) for the bucket. Accepts a single URL or an array.
bucket.provideBucketPublicUrl('https://pub-xxxx.r2.dev');
// or multiple custom domains
bucket.provideBucketPublicUrl(['https://cdn1.example.com', 'https://cdn2.example.com']);
console.log(bucket.getPublicUrls());
// ['https://pub-xxxx.r2.dev', 'https://cdn1.example.com', 'https://cdn2.example.com']Thanks to all contributors who have helped improve this project.