Read, write, stream, and presign files on S3-compatible object storage using Bun’s built-in S3 client — no npm package required.
Bun provides a fast, native S3 client that works with AWS S3, Cloudflare R2, DigitalOcean Spaces, MinIO, and any other S3-compatible storage service.The API is designed to feel like Bun’s local filesystem API — S3File extends Blob, so methods like .text(), .json(), .stream(), and .arrayBuffer() work the same way they do on local files.
import { s3 } from "bun";const file = s3.file("user-data/123.json");// Read from S3const data = await file.json();// Write to S3await file.write(JSON.stringify({ name: "Alice", score: 99 }), { type: "application/json",});// Generate a presigned download URL (synchronous, no network request)const url = file.presign({ expiresIn: 3600 });// Delete the fileawait file.delete();
For large files, writer() automatically uses S3 multipart upload. You don’t need to configure this explicitly — it happens whenever you stream data in chunks.
Presigned URLs grant time-limited access to a specific S3 object without exposing your credentials. Generating them is synchronous — no network request needed.
import { s3 } from "bun";// Default: GET, expires in 24 hoursconst downloadUrl = s3.presign("reports/q1.pdf");// Upload URL (PUT), expires in 1 hourconst uploadUrl = s3.presign("uploads/avatar.png", { method: "PUT", expiresIn: 3600, type: "image/png",});// Force download with a specific filenameconst attachmentUrl = s3.presign("reports/q1.pdf", { expiresIn: 3600, contentDisposition: 'attachment; filename="Q1 Report.pdf"',});// Public read accessconst publicUrl = s3.file("public/logo.png").presign({ acl: "public-read", expiresIn: 60 * 60 * 24 * 7, // 1 week});
import { S3Client } from "bun";// List up to 1000 objectsconst result = await S3Client.list(null, credentials);// List with a prefix and paginationconst uploads = await S3Client.list( { prefix: "uploads/", maxKeys: 100 }, credentials,);if (uploads.isTruncated) { const more = await S3Client.list( { prefix: "uploads/", maxKeys: 100, startAfter: uploads.contents!.at(-1)!.key, }, credentials, );}