hono-api-scaffolder
Scaffold Hono API routes for Cloudflare Workers. Produces route files, middleware, typed bindings, Zod validation, error handling, and API_ENDPOINTS.md documentation. Use after a project is set up with cloudflare-worker-builder or vite-flare-starter, when you need to add API routes, create endpoints, or generate API documentation.
Unlock Deep Analysis
Use AI to visualize the workflow and generate a realistic output preview for this skill.
Powered by Fastest LLM
Hono API Scaffolder
Add structured API routes to an existing Cloudflare Workers project. This skill runs AFTER the project shell exists (via cloudflare-worker-builder or vite-flare-starter) and produces route files, middleware, and endpoint documentation.
Workflow
Step 1: Gather Endpoints
Determine what the API needs. Either ask the user or infer from the project description. Group endpoints by resource:
Users: GET /api/users, GET /api/users/:id, POST /api/users, PUT /api/users/:id, DELETE /api/users/:id
Posts: GET /api/posts, GET /api/posts/:id, POST /api/posts, PUT /api/posts/:id
Auth: POST /api/auth/login, POST /api/auth/logout, GET /api/auth/me
Step 2: Create Route Files
One file per resource group. Use the template from assets/route-template.ts:
// src/routes/users.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import type { Env } from '../types'
const app = new Hono<{ Bindings: Env }>()
// GET /api/users
app.get('/', async (c) => {
const db = c.env.DB
const { results } = await db.prepare('SELECT * FROM users').all()
return c.json({ users: results })
})
// GET /api/users/:id
app.get('/:id', async (c) => {
const id = c.req.param('id')
const user = await db.prepare('SELECT * FROM users WHERE id = ?').bind(id).first()
if (!user) return c.json({ error: 'Not found' }, 404)
return c.json({ user })
})
// POST /api/users
const createUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
})
app.post('/', zValidator('json', createUserSchema), async (c) => {
const body = c.req.valid('json')
// ... insert logic
return c.json({ user }, 201)
})
export default app
Step 3: Add Middleware
Based on project needs, add from assets/middleware-template.ts:
Auth middleware — protect routes requiring authentication:
import { createMiddleware } from 'hono/factory'
import type { Env } from '../types'
export const requireAuth = createMiddleware<{ Bindings: Env }>(async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) return c.json({ error: 'Unauthorized' }, 401)
// Validate token...
await next()
})
CORS — use Hono's built-in:
import { cors } from 'hono/cors'
app.use('/api/*', cors({ origin: ['https://example.com'] }))
Step 4: Wire Routes
Mount all route groups in the main entry point:
// src/index.ts
import { Hono } from 'hono'
import type { Env } from './types'
import users from './routes/users'
import posts from './routes/posts'
import auth from './routes/auth'
import { errorHandler } from './middleware/error-handler'
const app = new Hono<{ Bindings: Env }>()
// Global error handler
app.onError(errorHandler)
// Mount routes
app.route('/api/users', users)
app.route('/api/posts', posts)
app.route('/api/auth', auth)
// Health check
app.get('/api/health', (c) => c.json({ status: 'ok' }))
export default app
Step 5: Create Types
// src/types.ts
export interface Env {
DB: D1Database
KV: KVNamespace // if needed
R2: R2Bucket // if needed
API_SECRET: string // secrets
}
Step 6: Generate API_ENDPOINTS.md
Document all endpoints. See references/endpoint-docs-template.md for the format:
## POST /api/users
Create a new user.
- **Auth**: Required (Bearer token)
- **Body**: `{ name: string, email: string }`
- **Response 201**: `{ user: User }`
- **Response 400**: `{ error: string, details: ZodError }`
Key Patterns
Zod Validation
Always validate request bodies with @hono/zod-validator:
import { zValidator } from '@hono/zod-validator'
app.post('/', zValidator('json', schema), async (c) => {
const body = c.req.valid('json') // fully typed
})
Install: pnpm add @hono/zod-validator zod
Error Handling
Use the standard error handler from assets/error-handler.ts:
export const errorHandler = (err: Error, c: Context) => {
console.error(err)
return c.json({ error: err.message }, 500)
}
API routes must return JSON errors, not redirects. fetch() follows redirects silently, then the client tries to parse HTML as JSON.
RPC Type Safety
For end-to-end type safety between Worker and client:
// Worker: export the app type
export type AppType = typeof app
// Client: use hc (Hono Client)
import { hc } from 'hono/client'
import type { AppType } from '../worker/src/index'
const client = hc<AppType>('https://api.example.com')
const res = await client.api.users.$get() // fully typed
Route Groups vs Single File
| Project size | Structure |
|---|---|
| < 10 endpoints | Single index.ts with all routes |
| 10-30 endpoints | Route files per resource (routes/users.ts) |
| 30+ endpoints | Route files + shared middleware + typed context |
Reference Files
| When | Read |
|---|---|
| Hono patterns, middleware, RPC | references/hono-patterns.md |
| API_ENDPOINTS.md format | references/endpoint-docs-template.md |
Assets
| File | Purpose |
|---|---|
| assets/route-template.ts | Starter route file with CRUD + Zod |
| assets/middleware-template.ts | Auth middleware template |
| assets/error-handler.ts | Standard JSON error handler |
Referenced Files
The following files are referenced in this skill and included for context.
assets/route-template.ts
/**
* Route Template — [Resource Name]
*
* Copy this file to src/routes/[resource].ts and customise.
* Includes: list, get, create, update, delete with Zod validation.
*/
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import type { Env } from '../types'
const app = new Hono<{ Bindings: Env }>()
const createSchema = z.object({
name: z.string().min(1).max(100),
// Add fields here
})
const updateSchema = createSchema.partial()
// GET /api/[resource]
app.get('/', async (c) => {
const db = c.env.DB
const { results } = await db.prepare('SELECT * FROM [table] ORDER BY created_at DESC').all()
return c.json({ items: results })
})
// GET /api/[resource]/:id
app.get('/:id', async (c) => {
const id = c.req.param('id')
const item = await c.env.DB.prepare('SELECT * FROM [table] WHERE id = ?').bind(id).first()
if (!item) return c.json({ error: 'Not found' }, 404)
return c.json({ item })
})
// POST /api/[resource]
app.post('/', zValidator('json', createSchema), async (c) => {
const body = c.req.valid('json')
const id = crypto.randomUUID()
await c.env.DB.prepare('INSERT INTO [table] (id, name) VALUES (?, ?)').bind(id, body.name).run()
return c.json({ item: { id, ...body } }, 201)
})
// PUT /api/[resource]/:id
app.put('/:id', zValidator('json', updateSchema), async (c) => {
const id = c.req.param('id')
const body = c.req.valid('json')
// ... update logic
return c.json({ item: { id, ...body } })
})
// DELETE /api/[resource]/:id
app.delete('/:id', async (c) => {
const id = c.req.param('id')
await c.env.DB.prepare('DELETE FROM [table] WHERE id = ?').bind(id).run()
return c.body(null, 204)
})
export default app
assets/middleware-template.ts
/**
* Auth Middleware Template
*
* Validates Bearer token and sets userId/role in context.
* Customise the token validation logic for your auth system.
*/
import { createMiddleware } from 'hono/factory'
import type { Env } from '../types'
type AuthVariables = {
userId: string
role: string
}
export const requireAuth = createMiddleware<{
Bindings: Env
Variables: AuthVariables
}>(async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
// TODO: Validate token (JWT, session lookup, API key check, etc.)
// const decoded = await verifyToken(token, c.env.JWT_SECRET)
// c.set('userId', decoded.sub)
// c.set('role', decoded.role)
await next()
})
references/endpoint-docs-template.md
# API Endpoints Documentation Template
Use this format when generating API_ENDPOINTS.md for a project.
## Template
```markdown
# API Endpoints
Base URL: `https://[project].workers.dev` (production) | `http://localhost:5173` (dev)
## Authentication
[Describe auth method: Bearer token, session cookie, API key, none]
## Endpoints
### [Resource Name]
#### GET /api/[resource]
List all [resources].
- **Auth**: [Required/None]
- **Query params**:
- `page` (number, default: 1)
- `limit` (number, default: 20, max: 100)
- `search` (string, optional)
- **Response 200**:
```json
{
"[resources]": [...],
"total": 42,
"page": 1,
"limit": 20
}
GET /api/[resource]/:id
Get a single [resource] by ID.
- Auth: [Required/None]
- Response 200:
{ "[resource]": { ... } } - Response 404:
{ "error": "Not found" }
POST /api/[resource]
Create a new [resource].
- Auth: Required
- Body:
{ "name": "string (required)", "email": "string (required, email format)" } - Response 201:
{ "[resource]": { ... } } - Response 400:
{ "error": "Validation failed", "details": { ... } }
PUT /api/[resource]/:id
Update an existing [resource].
- Auth: Required
- Body: Same as POST (all fields optional)
- Response 200:
{ "[resource]": { ... } } - Response 404:
{ "error": "Not found" }
DELETE /api/[resource]/:id
Delete a [resource].
- Auth: Required
- Response 204: (no body)
- Response 404:
{ "error": "Not found" }
Error Format
All errors return JSON:
{
"error": "Human-readable error message",
"details": {} // optional, present for validation errors
}
| Status | Meaning |
|---|---|
| 400 | Bad request / validation error |
| 401 | Not authenticated |
| 403 | Not authorised |
| 404 | Resource not found |
| 500 | Internal server error |
## Guidelines
- Document every endpoint, including auth requirements
- Show example request bodies with field types and constraints
- Show example response shapes (JSON)
- Include all possible error responses
- List query parameters with defaults and limits
- Keep descriptions to one line per endpoint
assets/error-handler.ts
/**
* Standard Error Handler
*
* Returns JSON errors for all routes.
* API routes must return JSON, never HTML redirects.
*/
import type { Context } from 'hono'
import { HTTPException } from 'hono/http-exception'
export const errorHandler = (err: Error, c: Context) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status)
}
console.error('Unhandled error:', err)
return c.json({ error: 'Internal server error' }, 500)
}
references/hono-patterns.md
# Hono Patterns
Advanced patterns for Hono on Cloudflare Workers. Load this when building complex APIs or troubleshooting route issues.
## Route Priority
Hono matches routes in registration order. Place specific routes before generic ones:
```typescript
// Correct order
app.get('/api/users/me', getMeHandler) // specific first
app.get('/api/users/:id', getUserHandler) // param route second
app.get('/api/users', listUsersHandler) // list last
Middleware Chains
Middleware runs in order of app.use() registration:
app.use('*', logger()) // all routes
app.use('/api/*', cors()) // all API routes
app.use('/api/admin/*', requireAuth) // admin routes only
Per-Route Middleware
app.get('/api/secret', requireAuth, async (c) => {
// runs after auth middleware
})
Typed Context with Variables
// Set in middleware
export const authMiddleware = createMiddleware<{
Bindings: Env
Variables: { userId: string; role: string }
}>(async (c, next) => {
c.set('userId', decoded.sub)
c.set('role', decoded.role)
await next()
})
// Read in handler
app.get('/api/me', authMiddleware, (c) => {
const userId = c.get('userId') // typed string
})
Request Handling
Path Parameters
app.get('/api/posts/:id', (c) => {
const id = c.req.param('id') // string
})
// Multiple params
app.get('/api/orgs/:orgId/users/:userId', (c) => {
const { orgId, userId } = c.req.param()
})
Query Parameters
app.get('/api/users', (c) => {
const page = parseInt(c.req.query('page') || '1')
const limit = parseInt(c.req.query('limit') || '20')
const search = c.req.query('search')
})
Headers
const token = c.req.header('Authorization')?.replace('Bearer ', '')
const contentType = c.req.header('Content-Type')
Response Patterns
JSON (most common)
return c.json({ users }, 200)
return c.json({ error: 'Not found' }, 404)
return c.json({ user }, 201) // created
Empty responses
return c.body(null, 204) // no content (DELETE success)
return new Response(null, { status: 204 })
Redirects
return c.redirect('/login', 302)
Streaming
return c.stream(async (stream) => {
await stream.write('chunk 1')
await stream.write('chunk 2')
})
Error Handling
Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return c.json({ error: err.message }, err.status)
}
console.error(err)
return c.json({ error: 'Internal server error' }, 500)
})
HTTPException
import { HTTPException } from 'hono/http-exception'
throw new HTTPException(403, { message: 'Forbidden' })
Not Found handler
app.notFound((c) => c.json({ error: 'Not found' }, 404))
Zod Validation Patterns
Body validation
const schema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'editor', 'viewer']).optional(),
})
app.post('/', zValidator('json', schema), async (c) => {
const body = c.req.valid('json')
})
Query validation
const querySchema = z.object({
page: z.coerce.number().min(1).default(1),
limit: z.coerce.number().min(1).max(100).default(20),
search: z.string().optional(),
})
app.get('/', zValidator('query', querySchema), async (c) => {
const { page, limit, search } = c.req.valid('query')
})
Custom error response
app.post('/',
zValidator('json', schema, (result, c) => {
if (!result.success) {
return c.json({ error: 'Validation failed', details: result.error.flatten() }, 400)
}
}),
handler,
)
RPC (Remote Procedure Call)
End-to-end type safety between Worker and client without code generation:
// Worker: chain routes for type inference
const routes = app
.get('/api/users', async (c) => c.json({ users: [] }))
.post('/api/users', zValidator('json', schema), async (c) => c.json({ user: {} }, 201))
export type AppType = typeof routes
// Client:
import { hc } from 'hono/client'
import type { AppType } from './worker'
const client = hc<AppType>('https://api.example.com')
const res = await client.api.users.$get()
const data = await res.json() // typed: { users: User[] }
Key: The route chain must be assigned to a variable for type inference to work. Don't use app.route() for RPC — mount routes directly on the app.
Source: https://github.com/jezweb/claude-skills#plugins-cloudflare-skills-hono-api-scaffolder
Content curated from original sources, copyright belongs to authors
User Rating
USER RATING
WORKS WITH