Repository navigation
Typed client for AWS S3 and S3-compatible object storage (Cloudflare R2, MinIO,
DigitalOcean Spaces, self-hosted gateways) for Deno, Bun, Node.js and Cloudflare
Workers, signing every request with AWS Signature Version 4.
Object CRUD and listing, plus streamed multipart uploads and downloads. A
lightweight, Web-API-only alternative to @aws-sdk/client-s3 for the operations
it covers.
S3 provides putObject, getObject, deleteObject, listObjects, and
headObject — the object-storage interface shared across this repo's
storage connects. It uses RESTler for transport and Guardian for runtime
response validation. Every request is signed with SigV4 entirely via Web
Crypto (crypto.subtle) — no node:crypto — so signing works unmodified
on Deno, Bun, Node, Cloudflare Workers, and in the browser.
Your tests don't need to fake HTTP. Have your code take an S3 instance
and pass in a stand-in that returns the shapes from
@tundraconnect/s3/schemas or throws a real S3Error:
import { S3Error } from '@tundraconnect/s3/errors';
const outage = new S3Error('SERVICE_UNAVAILABLE', { status: 503 });
console.log(outage.code);To exercise the client itself against a fake transport, subclass it and
reassign the protected _fetch.
| Topic | Description |
|---|---|
| API | Client configuration and endpoint methods |
| Errors | Error codes and diagnostic metadata |
| Schemas | Public Guardian schemas and inferred types |
- Amazon S3 API reference
- Create an AWS account
- Cloudflare R2 (S3-compatible)
- MinIO (S3-compatible, self-hosted)
- DigitalOcean Spaces (S3-compatible)
Deno:
deno add jsr:@tundraconnect/s3Bun:
bunx jsr add @tundraconnect/s3Node.js:
npx jsr add @tundraconnect/s3import { S3 } from '@tundraconnect/s3';
const client = new S3({
auth: {
type: 'CUSTOM',
accessKeyId: 'AKIA...',
secretAccessKey: '...',
region: 'us-east-1',
},
});
await client.putObject({
bucket: 'my-bucket',
key: 'hello.txt',
body: 'Hello!',
});
const obj = await client.getObject({ bucket: 'my-bucket', key: 'hello.txt' });
console.log(await obj.body.text());A custom baseURL automatically switches to path-style addressing (a
bucket subdomain won't resolve against most non-AWS endpoints):
import { S3 } from '@tundraconnect/s3';
const r2 = new S3({
baseURL: 'https://<account-id>.r2.cloudflarestorage.com',
auth: {
type: 'CUSTOM',
accessKeyId: '...',
secretAccessKey: '...',
region: 'auto',
},
});Unlike R2/MinIO, Spaces addresses buckets by subdomain — the same
virtual-hosted-style AWS S3 itself uses — even though it also needs a
custom baseURL. That combination is exactly what the forcePathStyle
auto-detection heuristic above doesn't expect, so set it to false
explicitly. The signing region is the literal Spaces region code (e.g.
nyc3, sfo3, ams3, sgp1, fra1), not an AWS region:
import { S3 } from '@tundraconnect/s3';
const spaces = new S3({
baseURL: 'https://nyc3.digitaloceanspaces.com',
forcePathStyle: false,
auth: {
type: 'CUSTOM',
accessKeyId: '...', // Spaces access key
secretAccessKey: '...', // Spaces secret key
region: 'nyc3',
},
});
await spaces.putObject({
bucket: 'my-space',
key: 'hello.txt',
body: 'Hello!',
});This resolves requests against
https://my-space.nyc3.digitaloceanspaces.com/hello.txt. Reads can instead
be served through the Spaces CDN by pointing baseURL (and, for signed
requests, only the read path) at
https://nyc3.cdn.digitaloceanspaces.com — putObject/deleteObject/
listObjects still need the origin endpoint above, since the CDN mirror is
read-only.
import { S3 } from '@tundraconnect/s3';
const client = new S3({
auth: {
type: 'CUSTOM',
accessKeyId: 'AKIA...',
secretAccessKey: '...',
region: 'us-east-1',
},
});
// Upload from a stream as a multipart upload — one 8 MiB part in memory
// at a time. Small bodies fall back to a single PUT automatically.
const file = await Deno.open('backup.tar');
await client.putObjectStream({
bucket: 'backups',
key: 'backup.tar',
body: file.readable,
});
// Download as a stream — nothing buffered.
const { body } = await client.getObjectStream({
bucket: 'backups',
key: 'backup.tar',
});
await body.pipeTo((await Deno.create('restored.tar')).writable);Works unchanged against DigitalOcean Spaces, Cloudflare R2 and MinIO. See API → Streaming.
MIT. Part of Tundra Connect, typed vendor API clients for Deno, Bun, Node.js and Cloudflare Workers.
- Algolia
- AzureBlob
- CloudflareDNS
- CloudflareEmail
- CloudflareSaaS
- CloudflareTurnstile
- CoinGecko
- Discord
- DodoPayments
- GCS
- GoogleAnalytics
- GoogleCertificateManager
- GoogleWebRisk
- Kalshi
- ntfy
- OpenExchange
- OpenWeatherMap
- PayPal
- Polymarket
- Razorpay
- Resend
- S3
- SendGrid
- Sentry
- Slack
- Stripe
- Telegram
- Twilio
- UpstashRedis
- URLhaus