npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

r2-storage-sdk

v1.1.0

Published

Official TypeScript SDK for the R2 Storage Platform - self-hosted object storage with chunked uploads, versioning, search, and analytics

Downloads

45

Readme

r2-storage-sdk

npm version npm downloads License: MIT CI

Official TypeScript SDK for the R2 Storage Platform — a self-hosted, Cloudflare R2–style object storage service. Store files directly on your own NVMe SSD with a professional dashboard, chunked uploads, versioning, search, and a full developer API.

Introduction

r2-storage-sdk is a zero-dependency, tree-shakeable TypeScript client for the R2 Storage Platform REST API. It runs in Node.js 18+ and modern browsers on top of the native fetch API, and ships both ESM and CommonJS builds with full type definitions and source maps.

Features

  • TypeScript first — fully typed, strict mode, IntelliSense everywhere
  • Dual ESM + CommonJS — works with bundlers, Node.js, and browsers
  • Zero runtime dependencies — the only thing it needs is fetch
  • Tree-shaking friendly — sideEffects: false, named exports, lazy facade
  • Chunked uploads — progress events, per-chunk retry, resume, and abort
  • Automatic retry — exponential backoff with jitter on 429 / 5xx / network errors, honors Retry-After
  • Typed errors — StorageApiError, StorageNetworkError, StorageUploadError, StorageAuthError
  • Interceptors — request and response hooks
  • API key or bearer token authentication, optional JWT refresh flow
  • Pagination — async-iterable Paginator for listing buckets, folders, and files

Installation

npm install r2-storage-sdk
# or
yarn add r2-storage-sdk
# or
pnpm add r2-storage-sdk

Quick Start

import { createClient } from 'r2-storage-sdk';

// The API endpoint is pre-configured — pass baseUrl only to override it.
const storage = createClient({
  apiKey: 'r2_your_api_key', // or accessToken for JWT auth
});

// Create a bucket
const bucket = await storage.bucket.create({ name: 'my-bucket' });

// Upload a file (single request, best for files under ~5 MB)
const file = await storage.file.upload(bucket.id, someBlob, { isPublic: true });

// Upload a large file (chunked with progress)
await storage.upload.upload({
  file: largeBlob,
  bucketId: bucket.id,
  fileName: 'video.mp4',
  onProgress: (e) => console.log(`${e.percentage}% at ${e.speed} B/s`),
});

// Download a file
const response = await storage.file.download(bucket.id, file.id);
const data = await response.arrayBuffer();

// Search
const results = await storage.search.files('report', { bucketId: bucket.id });

// Analytics
const trends = await storage.analytics.trends(30);

Authentication

// API key
const client = createClient({ apiKey: 'r2_...' });

// JWT bearer token
const client = createClient({ accessToken: 'jwt...' });

// Login / register to obtain tokens
const result = await client.auth.login({ email, password });
client.setTokens(result.accessToken, result.refreshToken);

// Verify the current session
const ok = await client.auth.verify();

// Log out (clears stored tokens)
await client.auth.logout();

When tokens are refreshed by your backend, the optional onTokenRefresh hook lets you persist the new pair:

const client = createClient({
  refreshToken: 'jwt...',
  onTokenRefresh: ({ accessToken, refreshToken }) => {
    localStorage.setItem('access', accessToken);
    localStorage.setItem('refresh', refreshToken);
  },
});

API Examples

Buckets

await storage.bucket.create({ name: 'photos', visibility: 'private' });
await storage.bucket.update('b1', { description: 'My photos' });
const page = await storage.bucket.list({ page: 1, pageSize: 20 });
const stats = await storage.bucket.getStats('b1');
await storage.bucket.delete('b1');

Folders

const folder = await storage.folder.create('b1', { name: '2026' });
await storage.folder.move('b1', folder.id, 'archive');
const crumbs = await storage.folder.breadcrumbs('b1', folder.id);
await storage.folder.delete('b1', folder.id);

Files

const file = await storage.file.upload('b1', blob, {
  folderId: 'f1',
  isPublic: true,
  metadata: { author: 'me' },
});
await storage.file.rename('b1', file.id, 'renamed.pdf');
await storage.file.move('b1', file.id, 'f2');
const { copied } = await storage.file.bulkCopy('b1', {
  fileIds: ['a', 'b'],
  targetBucketId: 'b2',
});
const versions = await storage.file.versions('b1', file.id);
await storage.file.delete('b1', file.id);

Chunked uploads

const result = await storage.upload.upload({
  file: largeBlob, // Blob | Buffer
  bucketId,
  fileName: 'movie.mp4',
  chunkSize: 5 * 1024 * 1024,
  concurrency: 2,
  onProgress: (e) => console.log(`${e.percentage}%`, e.speed, 'B/s', 'ETA', e.eta, 's'),
  signal: abortController.signal, // abort support
});

Pass an existing sessionId to resume an interrupted upload — already received chunks are skipped automatically.

Manual chunk control

const { sessionId } = await storage.upload.initiate({ ... });
await storage.upload.uploadChunk(sessionId, 0, chunkBlob);
const file = await storage.upload.complete(sessionId);
const status = await storage.upload.status(sessionId);
await storage.upload.cancel(sessionId);

Error Handling

Every failed call rejects with a typed StorageError subclass. Use the provided guards to branch on the failure mode:

import {
  isStorageError,
  isStorageApiError,
  StorageApiError,
  StorageNetworkError,
} from 'r2-storage-sdk';

try {
  await storage.bucket.get('missing');
} catch (err) {
  if (isStorageApiError(err)) {
    // { statusCode, code, retryable, retryAfterMs }
    if (err.statusCode === 404) console.log('Not found');
  } else if (err instanceof StorageNetworkError) {
    console.log('Network problem:', err.message);
  } else if (isStorageError(err)) {
    console.log('Storage error:', err.message);
  }
}
  • StorageApiError — the API returned a non-2xx status. Carries statusCode, optional code, and retryAfterMs parsed from the Retry-After header.
  • StorageNetworkError — the request never completed (DNS, timeout, abort). Always retryable.
  • StorageUploadError — chunked upload failed or was aborted (UPLOAD_ABORTED).
  • StorageAuthError — authentication failure.

Retries are automatic for 429/5xx/network errors with exponential backoff and jitter. Configure them per client:

createClient({
  apiKey,
  maxRetries: 5,
  retryBaseDelayMs: 500,
});

TypeScript Usage

The package is fully typed. Types are exported from the package root:

import {
  createClient,
  type Page,
  type Bucket,
  type File,
  type UploadProgressEvent,
} from 'r2-storage-sdk';

const page: Page<Bucket> = await storage.bucket.list();
for (const bucket of page.items) {
  console.log(bucket.name, bucket.visibility);
}

Pagination with Paginator

Paginator implements AsyncIterable, so you can stream all items without managing cursors:

import { Paginator } from 'r2-storage-sdk';

const paginator = new Paginator<File>((params) => storage.file.list('b1', params));

for await (const file of paginator) {
  console.log(file.name);
}
const total = await paginator.getTotal();

Advanced Examples

Global convenience facade

The package also ships a pre-configured storage facade for quick scripting and single-client apps. The facade is lazy — nothing is constructed until first use.

import { storage } from 'r2-storage-sdk';

storage.setApiKey('r2_your_api_key');
await storage.bucket.create({ name: 'demo' });

// create an isolated client with its own options
const isolated = storage({ apiKey: 'another-key' });

Interceptors

const client = createClient({ accessToken });

client.addRequestInterceptor((config) => ({
  ...config,
  headers: { ...config.headers, 'x-trace-id': crypto.randomUUID() },
}));

client.addResponseInterceptor((response) => {
  response.headers.set('x-sdk', '1');
  return response;
});

Low-level requests

const user = await client.request<{ id: string }>({
  method: 'GET',
  url: '/api/auth/me',
});
const raw: Response = await client.requestRaw({
  method: 'GET',
  url: '/api/buckets/b1/files/f1/download',
});

Signed download URLs

const url = await storage.file.getDownloadUrl('b1', 'file-1');
// https://storage.example.com/api/buckets/b1/files/file-1/download?token=...

Documentation

FAQ

Does the SDK require Node.js? No — it runs in Node.js 18+ and modern browsers (anything with fetch, FormData, and Blob). In browsers it can even persist tokens to localStorage.

Are there runtime dependencies? No. The published package has zero runtime dependencies. All HTTP is built on the global fetch API.

Can I use it with CommonJS? Yes. The package ships dist/index.cjs (CommonJS) and dist/index.js (ESM) with separate .d.cts / .d.ts typings, wired through the exports map.

Which Node versions are supported? Node.js 18 and above (engines.node >= 18).

Is tree-shaking effective? Yes. The package declares sideEffects: false, and the global storage facade is lazy, so bundlers can drop everything you don't import.

How do I abort an upload? Pass an AbortSignal to storage.upload.upload(). On abort, the server-side session is cancelled (best-effort) and a StorageUploadError with code UPLOAD_ABORTED is thrown.

How do I resume an interrupted upload? Keep the sessionId from initiate (or an earlier upload attempt) and pass it as sessionId on the next call. Already received chunks are skipped.

Contributing

Contributions are welcome! Open an issue for bugs and feature requests, and submit a pull request for changes. Please keep changes focused, add tests for new behavior, and run the full check suite before submitting.

Development setup

git clone https://github.com/r2-storage/storage-sdk.git
cd storage-sdk
npm install

npm run dev      # watch-mode build
npm run test     # run unit tests (Vitest)
npm run lint     # ESLint + Prettier check
npm run typecheck
npm run build

All code is formatted with Prettier and linted with ESLint. Run npm run lint:fix before committing.

Releasing

See RELEASE.md for the full release checklist. Releases are automated via GitHub Actions (npm run release:patch|minor|major bumps the version; tagging v* publishes to npm).

License

MIT © R2 Storage Platform

See LICENSE for details.