Bun 1.2 Ships a Native S3 Client That Works Across Alibaba, Tencent, and Huawei Cloud
Just these few lines:
import { S3Client } from "bun"
// Alibaba
const aliyun = new S3Client({
accessKeyId: process.env.ALI_KEY!,
secretAccessKey: process.env.ALI_SECRET!,
bucket: "my-bucket",
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
})
await aliyun.write("hello.txt", "兄弟们好啊")
// Tencent
const tx = new S3Client({
accessKeyId: process.env.TX_KEY!,
secretAccessKey: process.env.TX_SECRET!,
bucket: "my-bucket-1250000000",
endpoint: "https://cos.ap-guangzhou.myqcloud.com",
})
await tx.write("hello.txt", "兄弟们好啊")
// Huawei
const hw = new S3Client({
accessKeyId: process.env.HW_KEY!,
secretAccessKey: process.env.HW_SECRET!,
bucket: "my-bucket",
endpoint: "https://obs.cn-north-4.myhuaweicloud.com",
})
await hw.write("hello.txt", "兄弟们好啊")
The same Bun.S3 API works across all three clouds: Alibaba, Tencent, and Huawei.
bun run demo.ts
Done.
Pain Point: Every Cloud Has Its Own SDK
Folks, anyone who has done backend uploads and downloads knows this.
For file storage, every cloud vendor, domestic or international, writes its own SDK:
- Alibaba: OSS, uses
ali-oss - Tencent: COS, uses
cos-nodejs-sdk-v5 - Huawei: OBS, uses
esdk-obs-nodejs - AWS: S3, uses
aws-sdk
Want to switch vendors? Rewrite all your code from scratch.
Even worse, these SDKs:
- Are huge, with cold starts exceeding 80MB
- Have separate, confusing documentation
- Implement streaming uploads, multipart uploads, and signing algorithms differently
Your boss tells you to move to Huawei Cloud, and you're stuck staring at code written for Alibaba's SDK.
Solution: S3 Is the De Facto Industry Standard
S3 is the object storage service AWS launched back in 2006.
The S3 protocol has been so successful that it's now the industry's de facto standard.
What does de facto standard mean?
- AWS S3 itself supports it, of course
- Alibaba Cloud OSS is 100% S3-compatible
- Tencent Cloud COS is 100% S3-compatible
- Huawei Cloud OBS is 100% S3-compatible
- Cloudflare R2, MinIO, Qiniu, JD Cloud, Kingsoft Cloud are all compatible
Write one set of APIs; to switch clouds, just change the endpoint.
And Bun is the first to bake the S3 client directly into the runtime.
Bun.S3Client is a single class that works with all S3-compatible services.
This is a real capability only available since Bun 1.2. Node.js still has no native S3 support and requires installing the bulky aws-sdk.
In a Nutshell: What Is the S3 Protocol?
The S3 protocol is simply a set of HTTP REST APIs:
PUT /<bucket>/<key>: Upload a fileGET /<bucket>/<key>: Download a fileDELETE /<bucket>/<key>: Delete a fileLIST /<bucket>: List files
Every cloud vendor exposes these four verbs, plus authentication via AWS Signature v4.
As long as your code sends requests according to the S3 protocol, any cloud can handle them.
Alibaba OSS: The Endpoint Is Key
Alibaba Cloud OSS's S3-compatible endpoint looks like this:
import { S3Client } from "bun"
const oss = new S3Client({
accessKeyId: "LTAIxxxxxxxx",
secretAccessKey: "xxxxxxxx",
bucket: "my-bucket",
region: "oss-cn-hangzhou",
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
})
await oss.write("hello.txt", "兄弟们好啊")
const txt = await oss.file("hello.txt").text()
console.log(txt)
Gotcha: The bucket name must be globally unique. After creating a bucket in the Alibaba console, fill its name into the bucket field.
Use the oss-cn-hangzhou format for region. It's different from AWS's us-east-1, so don't copy it blindly.
Tencent COS: Bucket Name Includes APPID
Tencent COS is a bit special.
The bucket name must include the APPID suffix:
import { S3Client } from "bun"
const cos = new S3Client({
accessKeyId: "AKIDxxxxxxxx",
secretAccessKey: "xxxxxxxx",
bucket: "my-bucket-1250000000", // ← Note the APPID
region: "ap-guangzhou",
endpoint: "https://cos.ap-guangzhou.myqcloud.com",
})
await cos.write("hello.txt", "兄弟们好啊")
A number like 1250000000 is your Tencent Cloud account's APPID.
Forgot the APPID? You'll get a 404 error, bucket not found.
The console shows it when you create a bucket; just copy and paste.
Huawei Cloud OBS: The Endpoint Looks Very Similar
Huawei Cloud OBS is almost identical to AWS S3.
Even the region field format is copied directly:
import { S3Client } from "bun"
const obs = new S3Client({
accessKeyId: "HECxxxxxxxx",
secretAccessKey: "xxxxxxxx",
bucket: "my-bucket",
region: "cn-north-4",
endpoint: "https://obs.cn-north-4.myhuaweicloud.com",
})
await obs.write("hello.txt", "兄弟们好啊")
Huawei Cloud's region format is consistent with AWS:
cn-north-4Beijingcn-east-3Shanghaicn-south-1Guangzhou
Migrating from AWS to Huawei Cloud? Just change the endpoint.
One Codebase for All Three: The Factory Pattern
Folks, anyone who has built e-commerce, OA, or CRM systems knows this.
Multi-cloud adaptation is a hard requirement.
Encapsulate the three clouds into a factory:
import { S3Client } from "bun"
type Vendor = "aliyun" | "tencent" | "huawei"
const config: Record<Vendor, any> = {
aliyun: {
bucket: process.env.ALI_BUCKET!,
region: "oss-cn-hangzhou",
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
accessKeyId: process.env.ALI_KEY!,
secretAccessKey: process.env.ALI_SECRET!,
},
tencent: {
bucket: process.env.TX_BUCKET!, // includes APPID
region: "ap-guangzhou",
endpoint: "https://cos.ap-guangzhou.myqcloud.com",
accessKeyId: process.env.TX_KEY!,
secretAccessKey: process.env.TX_SECRET!,
},
huawei: {
bucket: process.env.HW_BUCKET!,
region: "cn-north-4",
endpoint: "https://obs.cn-north-4.myhuaweicloud.com",
accessKeyId: process.env.HW_KEY!,
secretAccessKey: process.env.HW_SECRET!,
},
}
export function getS3(vendor: Vendor) {
return new S3Client(config[vendor])
}
// Usage
const oss = getS3("aliyun")
await oss.write("hello.txt", "兄弟们好啊")
Business logic only calls getS3("aliyun"). No matter how the cloud vendor changes internally, not a single line outside needs modification.
Reading Files: text / json / stream
The S3File returned by S3Client.file() inherits from Blob.
Its usage is identical to Bun.file:
import { S3Client } from "bun"
const oss = new S3Client({
accessKeyId: process.env.ALI_KEY!,
secretAccessKey: process.env.ALI_SECRET!,
bucket: "my-bucket",
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
})
// Read JSON
const data = await oss.file("user.json").json()
// Read string
const text = await oss.file("readme.txt").text()
// Read bytes
const bytes = await oss.file("image.png").bytes()
// Stream large files
const stream = oss.file("big.log").stream()
for await (const chunk of stream) {
process.stdout.write(chunk)
}
Streaming saves memory; gigabyte-sized logs won't blow up your RAM.
Writing Files: Directly Saving Fetch Responses
The second argument to write() accepts a Response, allowing you to save network resources directly to the cloud:
import { S3Client } from "bun"
const oss = new S3Client({
accessKeyId: process.env.ALI_KEY!,
secretAccessKey: process.env.ALI_SECRET!,
bucket: "my-bucket",
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
})
// Download a web image and save it directly to Alibaba OSS
const res = await fetch("https://example.com/photo.jpg")
await oss.write("uploads/photo.jpg", res)
Before Bun, you had to write this:
// Old way: download to memory first, then upload
const res = await fetch("https://example.com/photo.jpg")
const buffer = Buffer.from(await res.arrayBuffer())
await oss.put("uploads/photo.jpg", buffer)
Bun streams the fetch response directly, with zero memory copy.
Presigned URLs: Temporary Direct Uploads
The tastiest feature: frontend uploads directly to the cloud, files never touch your server.
import { S3Client } from "bun"
const oss = new S3Client({
accessKeyId: process.env.ALI_KEY!,
secretAccessKey: process.env.ALI_SECRET!,
bucket: "my-bucket",
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
})
// An upload link that expires in 10 minutes
const uploadUrl = oss.file("uploads/avatar.jpg").presign({
method: "PUT",
expiresIn: 600,
type: "image/jpeg",
acl: "public-read",
})
// Send this URL to the frontend
console.log("Frontend can PUT to this URL:", uploadUrl)
Frontend code:
// Frontend
const file = document.querySelector("input").files[0]
await fetch(uploadUrl, {
method: "PUT",
body: file,
headers: { "Content-Type": "image/jpeg" },
})
This setup is incredibly cost-effective for user avatar uploads or document files.
The server only sends a URL. The file stream goes directly from the browser to the cloud, saving a huge amount on CDN traffic costs.
All three clouds support this; the signing algorithm follows the universal AWS v4 specification.
Listing & Deleting Files
Admin backends often need to clean up old files:
import { S3Client } from "bun"
const oss = new S3Client({
accessKeyId: process.env.ALI_KEY!,
secretAccessKey: process.env.ALI_SECRET!,
bucket: "my-bucket",
endpoint: "https://oss-cn-hangzhou.aliyuncs.com",
})
// List the first 100 files under uploads/
const list = await oss.list({ prefix: "uploads/", maxKeys: 100 })
for (const obj of list.contents ?? []) {
console.log(`${obj.key} → ${obj.size} bytes`)
}
// Pagination
if (list.isTruncated) {
const next = await oss.list({
prefix: "uploads/",
startAfter: list.contents!.at(-1)!.key,
})
}
// Check if a file exists
const exists = await oss.exists("uploads/1.png")
// Delete
await oss.delete("uploads/old.txt")
The same API works across all three clouds. Write the file listing logic once and never rewrite it.
Performance: 10x Faster Than aws-sdk
Bun's official tests, on identical hardware and network:
| Scenario | Bun.S3 | aws-sdk v3 |
|---|---|---|
| Small file reads | 3.2 GB/s | 0.3 GB/s |
| Large file streaming reads | 1.8 GB/s | 0.2 GB/s |
| Small file uploads | 1.5 GB/s | 0.15 GB/s |
About 10 times faster.
Bun uses a low-level HTTP client written in Zig, bypassing Node's V8 + libuv legacy path.
Cold start also drops from 80MB to 0, because Bun.S3Client is built-in and doesn't occupy node_modules.
Pitfall Guide
Folks, let's address a few pitfalls upfront:
1. Don't Write the Wrong Signing Region
Alibaba OSS uses oss-cn-hangzhou, Tencent COS uses ap-guangzhou, Huawei OBS uses cn-north-4, AWS uses us-east-1.
The formats differ between vendors; copying blindly will cause errors.
2. Tencent COS Buckets Include an APPID
my-bucket-1250000000 — the string of numbers 1250000000 cannot be omitted.
3. Alibaba OSS ACL Field Values Differ
// Alibaba
await oss.write("public.html", html, {
acl: "public-read",
type: "text/html",
})
ACL strings are generally universal across Alibaba, Tencent, and Huawei, but individual values differ; check the cloud vendor's documentation.
4. Path-style vs. Virtual-host-style
AWS defaults to virtual-host-style: https://my-bucket.s3.amazonaws.com/key
Alibaba, Tencent, and Huawei default to path-style: https://oss-cn-hangzhou.aliyuncs.com/my-bucket/key
Bun handles this automatically for you. Generally, you don't need to worry about it.
5. Bun Version
Bun ≥ 1.2.0 is required for the built-in S3 client.
bun --version
# 1.3.x is fine
When to Use Bun.S3?
| Scenario | Recommendation |
|---|---|
| New project, team uses Bun | Use Bun.S3 directly |
| Legacy project Node + aws-sdk | Don't touch it, stability first |
| Adapting to multiple domestic clouds | Use Bun.S3, just change the endpoint |
| Large file (GB-level) streaming | Use Bun.S3, automatic multipart |
| Frontend direct upload / temporary download links | Use Bun.S3, presign is synchronous |
Summary
The S3 protocol, created by AWS 20 years ago, is now the industry's de facto standard.
The three domestic giants:
- Alibaba OSS is 100% compatible
- Tencent COS is 100% compatible
- Huawei Cloud OBS is 100% compatible
A bunch of international ones:
- AWS S3 itself
- Cloudflare R2
- MinIO
- Google Cloud Storage
- DigitalOcean Spaces
Learn the S3 API, and one key opens 10 locks.
And Bun is the first to weld S3 directly into the runtime. Bun.S3Client is a single class that works with everything.
10x faster than aws-sdk, zero dependencies, zero cold start, uploads and downloads in 3 lines of code.
Folks, next time someone asks you "how do I interface with multiple domestic cloud storage providers," just throw this article at them and save yourself half an hour of explaining.
Found this useful? Hit like and forward.
Let's chat in the comments: which object storage provider does your company use? Have you been tortured by multi-cloud adaptation?
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
By that logic, why not just externalize all the configs? Do you even need the strategy pattern? Tomorrow a new cloud comes along, and you're stuck, right?