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
Maintainers
Readme
r2-storage-sdk
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
Paginatorfor listing buckets, folders, and files
Installation
npm install r2-storage-sdk
# or
yarn add r2-storage-sdk
# or
pnpm add r2-storage-sdkQuick 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. CarriesstatusCode, optionalcode, andretryAfterMsparsed from theRetry-Afterheader.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 buildAll 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.
