# R2 Common Patterns **Last Updated**: 2025-10-21 --- ## Image Upload & Serving ### Upload with Automatic Content-Type Detection ```typescript import { Hono } from 'hono'; type Bindings = { IMAGES: R2Bucket; }; const app = new Hono<{ Bindings: Bindings }>(); app.post('/upload/image', async (c) => { const formData = await c.req.formData(); const file = formData.get('image') as File; if (!file) { return c.json({ error: 'No file provided' }, 400); } // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif']; if (!allowedTypes.includes(file.type)) { return c.json({ error: 'Invalid file type' }, 400); } // Generate unique filename const extension = file.name.split('.').pop(); const filename = `${crypto.randomUUID()}.${extension}`; const key = `images/${filename}`; // Upload to R2 const arrayBuffer = await file.arrayBuffer(); const object = await c.env.IMAGES.put(key, arrayBuffer, { httpMetadata: { contentType: file.type, cacheControl: 'public, max-age=31536000, immutable', }, customMetadata: { originalFilename: file.name, uploadedAt: new Date().toISOString(), }, }); return c.json({ success: true, url: `/images/${filename}`, key: object.key, size: object.size, }); }); // Serve image app.get('/images/:filename', async (c) => { const filename = c.req.param('filename'); const key = `images/${filename}`; const object = await c.env.IMAGES.get(key); if (!object) { return c.json({ error: 'Image not found' }, 404); } return new Response(object.body, { headers: { 'Content-Type': object.httpMetadata?.contentType || 'image/jpeg', 'Cache-Control': 'public, max-age=31536000, immutable', 'ETag': object.httpEtag, }, }); }); export default app; ``` --- ## User File Storage with Folder Organization ```typescript app.post('/users/:userId/files', async (c) => { const userId = c.req.param('userId'); const formData = await c.req.formData(); const file = formData.get('file') as File; if (!file) { return c.json({ error: 'No file provided' }, 400); } // Organize by user ID and date const date = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const filename = file.name; const key = `users/${userId}/${date}/${filename}`; const arrayBuffer = await file.arrayBuffer(); const object = await c.env.MY_BUCKET.put(key, arrayBuffer, { httpMetadata: { contentType: file.type, contentDisposition: `attachment; filename="${filename}"`, }, customMetadata: { userId, uploadDate: date, originalSize: file.size.toString(), }, }); return c.json({ success: true, fileId: object.key, size: object.size, }); }); // List user's files app.get('/users/:userId/files', async (c) => { const userId = c.req.param('userId'); const cursor = c.req.query('cursor'); const listed = await c.env.MY_BUCKET.list({ prefix: `users/${userId}/`, limit: 100, cursor: cursor || undefined, }); return c.json({ files: listed.objects.map(obj => ({ key: obj.key, filename: obj.key.split('/').pop(), size: obj.size, uploaded: obj.uploaded, metadata: obj.customMetadata, })), hasMore: listed.truncated, cursor: listed.cursor, }); }); ``` --- ## Thumbnail Generation & Caching ```typescript app.get('/thumbnails/:filename', async (c) => { const filename = c.req.param('filename'); const width = parseInt(c.req.query('w') || '200'); const height = parseInt(c.req.query('h') || '200'); const thumbnailKey = `thumbnails/${width}x${height}/${filename}`; // Check if thumbnail already exists let thumbnail = await c.env.IMAGES.get(thumbnailKey); if (!thumbnail) { // Get original image const original = await c.env.IMAGES.get(`images/${filename}`); if (!original) { return c.json({ error: 'Image not found' }, 404); } // Generate thumbnail (using Cloudflare Images or external service) // This is a placeholder - use actual image processing const thumbnailData = await generateThumbnail( await original.arrayBuffer(), width, height ); // Store thumbnail for future requests await c.env.IMAGES.put(thumbnailKey, thumbnailData, { httpMetadata: { contentType: 'image/jpeg', cacheControl: 'public, max-age=31536000, immutable', }, }); thumbnail = await c.env.IMAGES.get(thumbnailKey); } return new Response(thumbnail!.body, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'public, max-age=31536000, immutable', }, }); }); async function generateThumbnail( imageData: ArrayBuffer, width: number, height: number ): Promise { // Use Cloudflare Images API, sharp, or other image processing library // This is a placeholder return imageData; } ``` --- ## Versioned File Storage ```typescript app.put('/files/:filename', async (c) => { const filename = c.req.param('filename'); const body = await c.req.arrayBuffer(); // Get current version number const versionKey = `versions/${filename}/latest`; const currentVersion = await c.env.MY_BUCKET.head(versionKey); let version = 1; if (currentVersion?.customMetadata?.version) { version = parseInt(currentVersion.customMetadata.version) + 1; } // Store new version const versionedKey = `versions/${filename}/v${version}`; await c.env.MY_BUCKET.put(versionedKey, body, { httpMetadata: { contentType: c.req.header('content-type') || 'application/octet-stream', }, customMetadata: { version: version.toString(), createdAt: new Date().toISOString(), }, }); // Update "latest" pointer await c.env.MY_BUCKET.put(versionKey, body, { httpMetadata: { contentType: c.req.header('content-type') || 'application/octet-stream', }, customMetadata: { version: version.toString(), latestVersion: 'true', }, }); return c.json({ success: true, version, key: versionedKey, }); }); // Get specific version app.get('/files/:filename/v/:version', async (c) => { const filename = c.req.param('filename'); const version = c.req.param('version'); const key = `versions/${filename}/v${version}`; const object = await c.env.MY_BUCKET.get(key); if (!object) { return c.json({ error: 'Version not found' }, 404); } return new Response(object.body, { headers: { 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream', }, }); }); ``` --- ## Backup & Archive Pattern ```typescript // Daily database backup to R2 async function backupDatabase(env: Bindings) { const date = new Date().toISOString().split('T')[0]; const key = `backups/database/${date}/dump.sql.gz`; // Generate backup (placeholder) const backupData = await generateDatabaseDump(); await env.BACKUPS.put(key, backupData, { httpMetadata: { contentType: 'application/gzip', contentEncoding: 'gzip', }, customMetadata: { backupDate: date, backupType: 'full', database: 'production', }, }); // Delete backups older than 30 days await cleanupOldBackups(env, 30); } async function cleanupOldBackups(env: Bindings, retentionDays: number) { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - retentionDays); const listed = await env.BACKUPS.list({ prefix: 'backups/database/', }); const oldBackups = listed.objects.filter( obj => obj.uploaded < cutoffDate ); if (oldBackups.length > 0) { const keysToDelete = oldBackups.map(obj => obj.key); await env.BACKUPS.delete(keysToDelete); } } ``` --- ## Static Site Hosting with SPA Fallback ```typescript app.get('/*', async (c) => { const url = new URL(c.req.url); let key = url.pathname.slice(1); // Remove leading slash if (key === '' || key.endsWith('/')) { key += 'index.html'; } let object = await c.env.STATIC.get(key); // SPA fallback: if file not found, try index.html if (!object && !key.includes('.')) { object = await c.env.STATIC.get('index.html'); } if (!object) { return c.json({ error: 'Not found' }, 404); } const headers = new Headers(); object.writeHttpMetadata(headers); // Set appropriate cache headers if (key.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$/)) { headers.set('Cache-Control', 'public, max-age=31536000, immutable'); } else { headers.set('Cache-Control', 'public, max-age=3600, must-revalidate'); } return new Response(object.body, { headers }); }); ``` --- ## CDN with Origin Fallback ```typescript // Use R2 as CDN with external origin fallback app.get('/cdn/*', async (c) => { const url = new URL(c.req.url); const key = url.pathname.replace('/cdn/', ''); // Check R2 cache first let object = await c.env.CDN_CACHE.get(key); if (!object) { // Fetch from origin const originUrl = `https://origin.example.com/${key}`; const response = await fetch(originUrl); if (!response.ok) { return c.json({ error: 'Not found on origin' }, 404); } const data = await response.arrayBuffer(); const contentType = response.headers.get('content-type') || 'application/octet-stream'; // Cache in R2 await c.env.CDN_CACHE.put(key, data, { httpMetadata: { contentType, cacheControl: 'public, max-age=31536000', }, }); object = await c.env.CDN_CACHE.get(key); } return new Response(object!.body, { headers: { 'Content-Type': object!.httpMetadata?.contentType || 'application/octet-stream', 'Cache-Control': 'public, max-age=31536000', 'X-Cache': object ? 'HIT' : 'MISS', }, }); }); ``` --- ## Signed Upload with Quota Limits ```typescript app.post('/request-upload', async (c) => { const { userId, filename, fileSize } = await c.req.json(); // Check user's quota const quota = await getUserQuota(userId); if (quota.used + fileSize > quota.total) { return c.json({ error: 'Quota exceeded' }, 403); } // Generate presigned URL const r2Client = new AwsClient({ accessKeyId: c.env.R2_ACCESS_KEY_ID, secretAccessKey: c.env.R2_SECRET_ACCESS_KEY, }); const key = `users/${userId}/${filename}`; const url = new URL( `https://my-bucket.${c.env.ACCOUNT_ID}.r2.cloudflarestorage.com/${key}` ); url.searchParams.set('X-Amz-Expires', '3600'); const signed = await r2Client.sign( new Request(url, { method: 'PUT' }), { aws: { signQuery: true } } ); return c.json({ uploadUrl: signed.url, expiresIn: 3600, }); }); async function getUserQuota(userId: string) { // Query database for user quota return { used: 1024 * 1024 * 100, // 100MB used total: 1024 * 1024 * 1024, // 1GB total }; } ``` --- ## Best Practices Summary 1. **Use meaningful key prefixes** for organization (`users/{id}/`, `images/`, `backups/`) 2. **Set appropriate cache headers** for static assets 3. **Store metadata** for tracking and filtering 4. **Use bulk delete** instead of loops 5. **Implement cleanup** for old/temporary files 6. **Add authentication** before presigned URL generation 7. **Validate file types** before uploading 8. **Use UUIDs** for unique filenames 9. **Set expiry times** on presigned URLs 10. **Monitor quota** to prevent overages