--- name: "hono-r2-integration" description: "Use this skill whenever the user wants to design, set up, or refactor Cloudflare R2 object storage usage in a Hono + TypeScript app running on Cloudflare Workers/Pages, including bucket bindings, upload/download flows, signed URLs, and folder-like organization." --- # Hono + Cloudflare R2 Integration Skill ## Purpose You are a specialized assistant for using **Cloudflare R2** (S3-compatible object storage) inside a **Hono + TypeScript** app running on **Cloudflare Workers / Pages**. Use this skill to: - Wire **R2 bucket bindings** into a Hono app (`c.env.BUCKET`) - Design **upload/download/delete** flows from Hono routes - Structure **storage service** modules on top of R2 - Handle **content types**, metadata, and folder-like key prefixes - Implement **presigned URL**-style access patterns where appropriate - Work safely within Workers constraints (streaming, body limits) Do **not** use this skill for: - Hono app scaffolding → use `hono-app-scaffold` - D1/database access → use `hono-d1-integration` - Frontend file handling/UI → use frontend/Next skills If `CLAUDE.md` exists, obey its rules for bucket naming, storage paths, and access patterns. --- ## When To Apply This Skill Trigger this skill when the user asks for things like: - “Use R2 to store files for this Hono API.” - “Add upload/download endpoints backed by R2.” - “Serve user avatars or documents from R2 in a Worker.” - “Organize stored files by user/tenant/date.” - “Refactor my raw R2 calls into a clean service.” Avoid this skill when: - Storage is not R2 (e.g., S3, Supabase Storage) for this service. - The project doesn’t run on Cloudflare Workers/Pages. --- ## Runtime & Binding Assumptions Assume: - App runs on **Cloudflare Workers** or **Pages Functions**. - R2 is configured as a binding in `wrangler.toml`, e.g.: ```toml [[r2_buckets]] binding = "BUCKET" bucket_name = "my-bucket" preview_bucket_name = "my-bucket-dev" ``` - In code, the bucket is available as `c.env.BUCKET` and has type `R2Bucket`. We type Env and encapsulate R2 access in services where possible. --- ## Project Structure This skill expects or creates something like: ```text src/ app.ts index.ts routes/ v1/ upload.routes.ts files.routes.ts storage/ r2-client.ts # helpers & Env typings file-storage.service.ts types/ env.d.ts # Env interface with BUCKET binding ``` Adjust paths to match the existing project. --- ## Typing Env and R2 Binding Define Env with an R2 bucket binding: ```ts // src/types/env.d.ts export interface Env { BUCKET: R2Bucket; // other bindings (DB, KV, etc) } ``` Integrate with Hono context typing: ```ts // src/types/app.ts import type { Env } from "./env"; export type AppContext = { Bindings: Env; }; ``` Use when creating the app: ```ts import { Hono } from "hono"; import type { AppContext } from "./types/app"; export const app = new Hono(); ``` This skill should ensure `c.env.BUCKET` is fully typed. --- ## R2 Helper Module Create a small helper to access the bucket and wrap common operations: ```ts // src/storage/r2-client.ts import type { Env } from "../types/env"; export function getBucket(env: Env): R2Bucket { return env.BUCKET; } export type R2ObjectInfo = { key: string; size: number; uploaded: string; etag?: string; httpEtag?: string; checksums?: Record; }; ``` This module can be extended with helper functions such as `putObject`, `getObject`, `deleteObject` etc., but often we call `bucket` methods directly from a service class. --- ## Storage Service Layer Create a service that receives `Env` and encapsulates R2 operations. ```ts // src/storage/file-storage.service.ts import type { Env } from "../types/env"; import { getBucket } from "./r2-client"; export class FileStorageService { constructor(private env: Env) {} private bucket() { return getBucket(this.env); } buildKey(prefix: string, filename: string): string { // Example: "users//" return `${prefix}/${filename}`; } async uploadObject( prefix: string, filename: string, body: ReadableStream | ArrayBuffer | Uint8Array, contentType?: string, ) { const key = this.buildKey(prefix, filename); const bucket = this.bucket(); const putResult = await bucket.put(key, body, { httpMetadata: contentType ? { contentType } : undefined, }); return { key, etag: putResult?.etag }; } async getObject(key: string) { const bucket = this.bucket(); const obj = await bucket.get(key); if (!obj) return null; return { key, body: obj.body, size: obj.size, uploaded: obj.uploaded, httpMetadata: obj.httpMetadata, }; } async deleteObject(key: string) { const bucket = this.bucket(); await bucket.delete(key); } async list(prefix: string) { const bucket = this.bucket(); const res = await bucket.list({ prefix }); return res.objects; } } ``` This skill should: - Encourage key naming conventions: e.g., `users//avatars/` or `tenants//...` - Use Stream bodies (`obj.body`) correctly for Workers. --- ## Hono Routes: Upload & Download ### Upload Route Example Supports `multipart/form-data` uploads using `c.req.parseBody()` or `c.req.formData()` depending on Hono version. ```ts // src/routes/v1/upload.routes.ts import { Hono } from "hono"; import type { AppContext } from "../../types/app"; import { FileStorageService } from "../../storage/file-storage.service"; export function uploadRoutes() { const app = new Hono(); app.post("/files", async (c) => { const formData = await c.req.formData(); const file = formData.get("file"); if (!(file instanceof File)) { return c.json({ message: "file field is required" }, 400); } const userId = "anonymous"; // or from auth middleware, e.g. c.get("user").id const storage = new FileStorageService(c.env); const key = storage.buildKey(`users/${userId}`, file.name); const arrayBuffer = await file.arrayBuffer(); await storage.uploadObject(`users/${userId}`, file.name, arrayBuffer, file.type); return c.json({ key }, 201); }); return app; } ``` This skill should: - Recommend secure naming (no direct user input as raw key without sanitizing). - Encourage deriving prefixes from authenticated user/tenant. ### Download Route Example ```ts // src/routes/v1/files.routes.ts import { Hono } from "hono"; import type { AppContext } from "../../types/app"; import { FileStorageService } from "../../storage/file-storage.service"; export function filesRoutes() { const app = new Hono(); app.get("/files/:key", async (c) => { const keyParam = c.req.param("key"); // You may want to decode / validate this; often we pass encoded keys or IDs instead const key = decodeURIComponent(keyParam); const storage = new FileStorageService(c.env); const obj = await storage.getObject(key); if (!obj) { return c.json({ message: "Not found" }, 404); } const headers: Record = {}; if (obj.httpMetadata?.contentType) { headers["Content-Type"] = obj.httpMetadata.contentType; } return new Response(obj.body, { status: 200, headers, }); }); return app; } ``` **Important:** Exposing raw keys in URLs may not be ideal; many apps use an opaque ID that maps to a key. This skill can recommend a mapping layer if needed. --- ## Presigned-URL Style Patterns (Optional) R2 doesn’t provide S3-style presigned URLs in the same way out of the box, but you can: - Generate a short-lived signed token (JWT) containing the key + permission. - Use a Hono route to validate that token and stream the object. Example idea: ```ts // pseudo-code app.get("/download/:token", async (c) => { const token = c.req.param("token"); const { key } = await verifyDownloadToken(token); // JWT or similar const storage = new FileStorageService(c.env); const obj = await storage.getObject(key); // stream object if valid }); ``` This skill should: - Suggest this pattern when the user wants “presigned URLs” behavior. - Defer actual token signing/verification to an auth/signing skill. --- ## Folder-Like Organization & Multi-Tenancy This skill should help you design key schemes like: - Per-user: - `users//avatars/` - `users//documents/.pdf` - Per-tenant: - `tenants//exports/.json` - Public vs private: - `public/` vs `private//` It should: - Encourage not leaking sensitive identifiers where not needed. - Suggest using random IDs or hashed filenames for users’ private content. --- ## Integration with Auth When `hono-authentication` is present, this skill can: - Use `c.get("user")` to determine `userId` or roles. - Enforce access control for: - Upload (who can write to which prefixes) - Download (who can read which keys) Example pattern: ```ts const user = c.get("user"); if (!user) return c.json({ message: "Unauthorized" }, 401); // build prefix from user.id const prefix = `users/${user.id}`; ``` --- ## Local Development & Testing This skill may recommend: - Using R2’s local dev via `wrangler dev` and preview buckets. - Mocking `R2Bucket` methods in unit tests: ```ts const mockBucket: Partial = { put: jest.fn().mockResolvedValue({ etag: "etag" } as any), get: jest.fn().mockResolvedValue(null), list: jest.fn().mockResolvedValue({ objects: [] } as any), delete: jest.fn().mockResolvedValue(undefined), }; ``` - For integration tests, run `wrangler dev` and hit endpoints that talk to R2 directly. --- ## Error Handling & Limits This skill must consider: - File size limits; large uploads should be constrained (and perhaps chunked in more advanced setups). - Graceful error handling when bucket operations fail: ```ts try { await bucket.put(key, body); } catch (err) { console.error("R2 upload failed:", err); return c.json({ message: "Upload failed" }, 500); } ``` - Avoid echoing internal R2 errors directly to clients. --- ## Example Prompts That Should Use This Skill - “Store user uploads in R2 from this Hono API.” - “Add endpoints to upload and download files using Cloudflare R2.” - “Organize R2 keys by user/tenant in my Workers app.” - “Refactor my R2 calls into a reusable service in Hono.” - “Implement a safe download route for R2-stored documents.” For these tasks, rely on this skill to build a **clean, typed, and secure R2 integration** inside your Hono + Cloudflare Workers/Pages application.