Cloudflare Hono API
This skill has one job: ask enough questions, then either create a new API or add a resource/routes. Do not start coding until the questions below are answered (from the user or from existing code).
Stack: Cloudflare Workers + Hono + D1 + Drizzle + Zod. Auth identity is Firebase. R2 only if the resource needs files.
Do not use Express, Firebase Functions, Firestore, or Joi. Do not use Firebase Admin on Workers. Do not mint Firebase custom tokens from the Worker.
Conventions and templates: reference.md.
1. Ask first
Ask only what you cannot infer from the repo. Stop and wait if anything material is missing.
Mode
- Create a new API (Worker + Hono app + auth + db wiring), or
- Add a resource / routes to an existing API?
Resource and routes
- Resource name (singular, lowercase), e.g.
task - Which operations:
POST /,GET /,GET /:id,PATCH /:id,DELETE /:id, extra/:id/actionroutes? - Request body fields for writes; list/filter query params
- Persist in D1? If yes, columns, indexes, unique constraints
Auth (default A unless they need B)
- Public routes vs authenticated
- A — Firebase ID token on each request (verify Bearer JWT via Google JWKS; set
{ uid, email }) - B — App token with custom claims — only if they need claims Firebase does not already provide. Ask which claims (e.g.
role,tenantId). Do not addPOST /auth/exchange"just in case"
Optional
- Role / admin checks on specific routes?
- Multi-tenant isolation (
tenant_idon rows and in queries)? - File upload/download (R2)?
2. Then implement
Read reference.md and follow it.
New API
Create the Worker skeleton only: src/index.ts, src/env.ts, src/db/ (if D1), src/auth/ (if any route is authenticated), Wrangler bindings. Then add each requested resource with the five-file pattern.
Auth mode B: add src/auth/appToken.ts and POST /auth/exchange only when claims were requested.
Add resource / routes
If the API already exists, match its src/ layout. Add or extend one resource folder. Mount new routers in index.ts. Add Drizzle tables + D1 migrations only for new persisted fields.
One resource folder, five files:
src/routes/<resource>/
├── <resource>Router.ts
├── <resource>Schema.ts
├── <resource>DataValidation.ts
├── <resource>DataAccess.ts
└── <resource>Service.ts
File suffix is DataValidation.ts.
Hard rules
- Routers chain middleware only. No DB calls in routers.
- Services own HTTP status and
try/catch. They call data access; they do not import Drizzle tables. - Data access owns Drizzle. One function = one DB or R2 operation. Prefixes:
db*(D1),r2*(R2). - Zod +
@hono/zod-validator. Bindings fromc.env, notprocess.env. - IDs:
crypto.randomUUID(), stored as a column. - Tenant-scoped tables: indexed
tenant_id; everydbGet*/dbList*filters by it. - Route chain:
auth → (role check) → (validate) → logic.
New resource checklist
- Drizzle table + migration if persisted
- Schema (types, enums, Zod bodies)
- Data access (
db*/r2*) - Service (
*Logic) - Validation (
validate*) - Router
app.route("/<resource>", resourceRouter)inindex.ts
Errors
| Condition | Status | Body |
|---|---|---|
| Missing/invalid token | 401 | { "error": "Unauthorized: ..." } |
| Authenticated but not allowed | 403 | { "error": "Forbidden: ..." } |
| Invalid body | 400 | { "error": "<zod message>" } |
| Missing row | 404 | { "error": "<resource> not found" } |
| Upstream failure | 502 | { "error": "Failed to ..." } |
| Unhandled | 500 | { "error": "Internal server error" } |
| Created | 201 | resource object |
| Read/update | 200 | resource object or message |