R2 Storage API
Upload and serve files with Cloudflare R2 storage - private and public buckets with list, get, put, and delete operations
Upload images and files to R2 with configurable private and public access. Objects in the private bucket are only accessible through the Worker, while public bucket objects get a direct URL.
Features
- Private bucket: Objects only accessible via Worker API (
GET /object?key=...) - Public bucket: Objects get a public URL (e.g.
https://pub-xxx.r2.dev/key) when uploaded withpublic=true - List operations with pagination, prefix filtering, and directory-style delimiters
- Custom metadata support via
X-Custom-Metadata-*headers - Content-Type handling for serving images and other media
API Reference
List objects
GET /list
Query Parameters
public boolean
Set to true or 1 to list from the public bucket. Omit for private bucket.
prefix string
Filter objects by key prefix (e.g., images/ to list only keys starting with "images/")
limit number
Maximum number of keys to return (1-1000)
cursor string
Pagination cursor from a previous response (when truncated: true)
delimiter string
Delimiter for directory-style listing (e.g., / to group by directory)
Response
objects array
Array of object metadata
truncated boolean
Whether there are more results available
cursor string
Cursor for next page (only present when truncated: true)
Example
# List all objects from private bucket
curl "https://your-worker.workers.dev/list"
# List public objects with prefix
curl "https://your-worker.workers.dev/list?public=true&prefix=images/"
# Paginated listing
curl "https://your-worker.workers.dev/list?limit=10&cursor=eyJrZXkiOi4uLn0"{
"objects": [
{
"key": "images/photo.png",
"size": 102400,
"etag": "abc123def456",
"uploaded": "2025-03-10T12:34:56.789Z"
}
],
"truncated": false
}Get object
GET /object
Download an object from R2. Returns the raw file with appropriate Content-Type.
Query Parameters
key string (required)
Object key to retrieve
public boolean
Set to true or 1 to get from the public bucket
Headers
param string
Optional byte range for partial content (e.g., bytes=0-1023)
Response
Returns the raw object body with headers:
Content-Type: From object's HTTP metadataETag: Object version identifierContent-Length: Size in bytes
Example
# Download private object
curl "https://your-worker.workers.dev/object?key=private/document.pdf" -o document.pdf
# Download public object
curl "https://your-worker.workers.dev/object?key=images/photo.png&public=true" -o photo.png
# Get partial content
curl -H "Range: bytes=0-1023" "https://your-worker.workers.dev/object?key=video.mp4"Get object metadata
HEAD /object
Query Parameters
key string (required)
Object key to check
public boolean
Set to true or 1 to check the public bucket
Response
key string
Object key
size number
Size in bytes
etag string
Entity tag
uploaded string
ISO 8601 timestamp
customMetadata object
Custom metadata if present
httpMetadata object
HTTP metadata including contentType
Example
curl -X HEAD "https://your-worker.workers.dev/object?key=photo.png&public=true"{
"key": "photo.png",
"size": 102400,
"etag": "abc123def456",
"uploaded": "2025-03-10T12:34:56.789Z",
"httpMetadata": {
"contentType": "image/png"
}
}Upload object
PUT /object
Query Parameters
key string (required)
Object key (path) for the upload
public boolean
Set to true or 1 to upload to the public bucket and get a public URL
Headers
param string
MIME type of the file (e.g., image/png, application/pdf)
param string
Custom metadata headers (e.g., X-Custom-Metadata-Author: John)
Request Body
Raw bytes of the file to upload.
Response
key string
Object key that was uploaded
uploaded boolean
Always true on success
url string
Public URL (only present for public bucket uploads when PUBLIC_BUCKET_URL is configured)
Example
# Upload to private bucket
curl -X PUT "https://your-worker.workers.dev/object?key=documents/report.pdf" \
-H "Content-Type: application/pdf" \
--data-binary @report.pdf
# Upload image to public bucket
curl -X PUT "https://your-worker.workers.dev/object?key=images/photo.png&public=true" \
-H "Content-Type: image/png" \
--data-binary @photo.png
# Upload with custom metadata
curl -X PUT "https://your-worker.workers.dev/object?key=files/data.json&public=true" \
-H "Content-Type: application/json" \
-H "X-Custom-Metadata-Author: Alice" \
-H "X-Custom-Metadata-Version: 1.0" \
--data-binary @data.json{
"key": "documents/report.pdf",
"uploaded": true
}{
"key": "images/photo.png",
"uploaded": true,
"url": "https://pub-xxxxx.r2.dev/images/photo.png"
}The url in the response is a direct public link. You can share it or use it in <img> tags without going through the Worker.
Delete object
DELETE /object
Query Parameters
key string (required)
Object key to delete
public boolean
Set to true or 1 to delete from the public bucket
Response
key string
Object key that was deleted
deleted boolean
Always true on success
Example
# Delete from private bucket
curl -X DELETE "https://your-worker.workers.dev/object?key=temp/old-file.txt"
# Delete from public bucket
curl -X DELETE "https://your-worker.workers.dev/object?key=images/old-photo.png&public=true"{
"key": "temp/old-file.txt",
"deleted": true
}Setup
Create R2 buckets
In the Cloudflare dashboard → R2 → Create bucket:
- Create
r2-storage-bucket(private - default settings) - Create
r2-storage-public(for public files)
Enable public access
For the public bucket only:
- Go to R2 → select
r2-storage-public→ Settings - Under Public Development URL, click Enable
- Type
allowwhen prompted and confirm - Copy the public bucket URL (looks like
https://pub-xxxxx.r2.dev)
You can also use a custom domain instead of the r2.dev URL
Configure wrangler.json
Update your wrangler.json with the bucket bindings:
{
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "r2-storage-bucket"
},
{
"binding": "PUBLIC_BUCKET",
"bucket_name": "r2-storage-public"
}
],
"vars": {
"PUBLIC_BUCKET_URL": "https://pub-xxxxx.r2.dev"
}
}Replace https://pub-xxxxx.r2.dev with your actual public bucket URL.
Run locally
cd apps/experiments/r2-storage
npm install
npm run dev -- --remoteUse --remote to connect to real R2 buckets. The public URL won't work with local buckets.
Error Responses
All errors return JSON with this structure:
{
"error": "Error message",
"code": "ERROR_CODE"
}error string
Human-readable error message
code string
Error code: INVALID_QUERY, NOT_FOUND, MISSING_PARAM, INTERNAL_ERROR
Source Code
View the complete source code on GitHub: r2-storage
Use Cases
Image hosting
Upload images to the public bucket and use the returned URL in your app:
# Upload
curl -X PUT "https://your-worker.workers.dev/object?key=avatars/user123.jpg&public=true" \
-H "Content-Type: image/jpeg" \
--data-binary @avatar.jpg
# Response: { "url": "https://pub-xxxxx.r2.dev/avatars/user123.jpg", ... }
# Use URL directly: <img src="https://pub-xxxxx.r2.dev/avatars/user123.jpg" />Private file storage
Store documents that should only be accessible through your Worker:
# Upload
curl -X PUT "https://your-worker.workers.dev/object?key=user-data/123/invoice.pdf" \
-H "Content-Type: application/pdf" \
--data-binary @invoice.pdf
# Download (with auth check in your Worker)
curl "https://your-worker.workers.dev/object?key=user-data/123/invoice.pdf" \
-H "Authorization: Bearer token"Organized file structure
Use prefixes and delimiters for directory-like organization:
# List all images
curl "https://your-worker.workers.dev/list?prefix=images/&public=true"
# List top-level "directories" only
curl "https://your-worker.workers.dev/list?delimiter=/&public=true"Limitations
- No authentication on API endpoints in this demo
- List operations return at most 1,000 keys per request
- Requires R2 bucket bindings and optional public URL configuration
- Not a multipart upload or large-file streaming demo
Deployment
Create buckets
Create both R2 buckets and enable public access on the public one (see Setup above)
Configure variables
In Workers & Pages → your Worker → Settings → Variables:
- Add
PUBLIC_BUCKET_URL= your public bucket URL (e.g.,https://pub-xxxxx.r2.dev)
Update wrangler.json
Ensure bucket_name values match your actual bucket names in the dashboard
Configuration
Environment Variables
param string
Base URL for the public R2 bucket (e.g., https://pub-xxxxx.r2.dev). Required to return public URLs in upload responses. Set in wrangler.json under vars or in the dashboard under Workers & Pages → Settings → Variables.
Bindings
param R2Bucket
Private R2 bucket binding (name: r2-storage-bucket)
param R2Bucket
Public R2 bucket binding (name: r2-storage-public)
Cloudflare Features Used
- Workers - Serverless compute at the edge
- R2 - Object storage with zero egress fees
- Public buckets - Direct public access to objects
- Hono - Lightweight web framework