Back to skills
SkillHub ClubShip Full StackFull StackBackend

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.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
618
Hot score
99
Updated
March 20, 2026
Overall rating
C4.6
Composite score
4.6
Best-practice grade
B78.7

Install command

npx @skill-hub/cli install jezweb-claude-skills-hono-api-scaffolder

Repository

jezweb/claude-skills

Skill path: plugins/cloudflare/skills/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.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Backend.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: jezweb.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install hono-api-scaffolder into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/jezweb/claude-skills before adding hono-api-scaffolder to shared team environments
  • Use hono-api-scaffolder for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: hono-api-scaffolder
description: "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."
compatibility: claude-code-only
---

# 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](assets/route-template.ts):

```typescript
// 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](assets/middleware-template.ts):

**Auth middleware** — protect routes requiring authentication:
```typescript
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:
```typescript
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:

```typescript
// 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

```typescript
// 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](references/endpoint-docs-template.md) for the format:

```markdown
## 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`:

```typescript
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](assets/error-handler.ts):

```typescript
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:

```typescript
// 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](references/hono-patterns.md) |
| API_ENDPOINTS.md format | [references/endpoint-docs-template.md](references/endpoint-docs-template.md) |

## Assets

| File | Purpose |
|------|---------|
| [assets/route-template.ts](assets/route-template.ts) | Starter route file with CRUD + Zod |
| [assets/middleware-template.ts](assets/middleware-template.ts) | Auth middleware template |
| [assets/error-handler.ts](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

```typescript
/**
 * 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

```typescript
/**
 * 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

```markdown
# 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**:
  ```json
  {
    "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:

```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

```typescript
/**
 * 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

```markdown
# 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:

```typescript
app.use('*', logger())                        // all routes
app.use('/api/*', cors())                     // all API routes
app.use('/api/admin/*', requireAuth)          // admin routes only
```

### Per-Route Middleware

```typescript
app.get('/api/secret', requireAuth, async (c) => {
  // runs after auth middleware
})
```

### Typed Context with Variables

```typescript
// 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

```typescript
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

```typescript
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

```typescript
const token = c.req.header('Authorization')?.replace('Bearer ', '')
const contentType = c.req.header('Content-Type')
```

## Response Patterns

### JSON (most common)

```typescript
return c.json({ users }, 200)
return c.json({ error: 'Not found' }, 404)
return c.json({ user }, 201)  // created
```

### Empty responses

```typescript
return c.body(null, 204)  // no content (DELETE success)
return new Response(null, { status: 204 })
```

### Redirects

```typescript
return c.redirect('/login', 302)
```

### Streaming

```typescript
return c.stream(async (stream) => {
  await stream.write('chunk 1')
  await stream.write('chunk 2')
})
```

## Error Handling

### Global error handler

```typescript
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

```typescript
import { HTTPException } from 'hono/http-exception'

throw new HTTPException(403, { message: 'Forbidden' })
```

### Not Found handler

```typescript
app.notFound((c) => c.json({ error: 'Not found' }, 404))
```

## Zod Validation Patterns

### Body validation

```typescript
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

```typescript
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

```typescript
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:

```typescript
// 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.

```

hono-api-scaffolder | SkillHub