R2 Storage
Cloudflare R2 is S3-compatible object storage with zero egress fees. Accessed via getBucket() inside server functions.
Drag & drop image upload
bucket.put(key, arrayBuffer, { httpMetadata })Drop images onto the zone below or click to browse. Files are read as base64 on the client, decoded to bytes on the server, and stored in R2 under the images/ prefix.
Drag & drop images, or click to browse
PNG, JPG, GIF, WebP, SVG
// Client: read file as base64
function fileToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = () => {
const dataUrl = reader.result as string
resolve(dataUrl.split(',')[1]) // strip data:...;base64, prefix
}
reader.onerror = reject
reader.readAsDataURL(file)
})
}
// Server: decode and upload to R2
const uploadImage = createServerFn({ method: 'POST' })
.handler(async ({ data }) => {
const bucket = getBucket()
const bytes = Uint8Array.from(atob(data.base64), c => c.charCodeAt(0))
await bucket.put(`images/${data.name}`, bytes, {
httpMetadata: { contentType: data.type },
})
})Image gallery
bucket.list({ prefix: 'images/' })Images are served via an API route at /api/r2?key=... that streams the R2 object body with the correct Content-Type header. Click an image to view full size.
No images yet. Upload some above.
// API route: src/routes/api/r2.ts
// Serves R2 objects by key query parameter
export const Route = createFileRoute('/api/r2')({
server: {
handlers: {
GET: async ({ request }) => {
const url = new URL(request.url)
const key = url.searchParams.get('key')
const bucket = getBucket()
const obj = await bucket.get(key)
if (!obj) return new Response('Not found', { status: 404 })
return new Response(obj.body, {
headers: {
'Content-Type': obj.httpMetadata?.contentType || 'application/octet-stream',
'Cache-Control': 'public, max-age=3600',
},
})
},
},
},
})
// Usage in JSX:
// <img src={`/api/r2?key=${encodeURIComponent(imageKey)}`} />Upload an object
bucket.put(key, body, options)Uses a POST server function. The key is the object path (supports slashes for folder-like structure). Content-type metadata is set via httpMetadata.
const uploadText = createServerFn({ method: 'POST' })
.inputValidator((data: { key: string; content: string }) => {
if (!data.key.trim()) throw new Error('Key is required')
return data
})
.handler(async ({ data }) => {
const bucket = getBucket()
await bucket.put(data.key, data.content, {
httpMetadata: { contentType: 'text/plain' },
})
return { key: data.key }
})Objects in bucket
bucket.list({ limit: 50 })Loaded via the route loader on first render (SSR). Each object shows its key, size, and upload date. The delete button calls bucket.delete(key).
hi.txt
Read an object
bucket.get(key) → obj.text()Fetch an object by key via a GET server function. The R2Object body can be consumed as text, json, arrayBuffer, or a ReadableStream.
const getObject = createServerFn({ method: 'GET' })
.inputValidator((data: { key: string }) => data)
.handler(async ({ data }) => {
const bucket = getBucket()
const obj = await bucket.get(data.key)
if (!obj) throw new Error('Not found')
const text = await obj.text() // or .json(), .arrayBuffer()
return {
body: text,
size: obj.size,
contentType: obj.httpMetadata?.contentType,
}
})Accessing R2 via getBucket()
src/lib/env.tsThe env helper provides typed access to the R2 bucket binding. Import getBucket() in any server function.
// src/lib/env.ts
import { env } from 'cloudflare:workers'
export function getEnv(): Env {
return env as Env
}
export function getBucket(): R2Bucket {
return getEnv().BUCKET
}// In any server function
import { getBucket } from '~/lib/env'
const myServerFn = createServerFn().handler(async () => {
const bucket = getBucket()
// Upload
await bucket.put('key', 'value')
// Read
const obj = await bucket.get('key')
const text = await obj?.text()
// List
const { objects } = await bucket.list({ prefix: 'notes/' })
// Delete
await bucket.delete('key')
})| Method | Returns | Use Case |
|---|---|---|
| put(key, value) | R2Object | Upload / overwrite |
| get(key) | R2ObjectBody | null | Read with body stream |
| head(key) | R2Object | null | Metadata only, no body |
| list(options) | R2Objects | Paginated listing with prefix filter |
| delete(key) | void | Remove object |
Setup checklist
wrangler.jsonc + wrangler typesSteps to add R2 to a new project.
- Create the bucket
wrangler r2 bucket create my-bucket
- Add the binding to wrangler.jsonc
"r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-bucket" }] - Regenerate types
wrangler types
- Use getBucket() in server functions
const bucket = getBucket() await bucket.put('hello.txt', 'Hello, R2!')
Binary uploads: For file uploads from the browser, send the file as FormData to a POST server function. Use request.formData() to parse it, then pass the File/ArrayBuffer directly to bucket.put(). R2 accepts strings, ArrayBuffers, ReadableStreams, and Blobs.