Scaffold a production-ready Hono 4.x API with TypeScript, Zod validation, OpenAPI generation, and multi-runtime support (Cloudflare Workers, Node.js, Bun).
90
87%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
Scaffold a production-ready Hono 4.x API with TypeScript, Zod validation, OpenAPI generation, and multi-runtime support (Cloudflare Workers, Node.js, Bun).
pnpm create hono@latest <project-name>
# Select "cloudflare-workers" template
cd <project-name>
pnpm add @hono/zod-openapi @hono/zod-validator zod
pnpm add -D @cloudflare/workers-typespnpm create hono@latest <project-name>
# Select "nodejs" template
cd <project-name>
pnpm add @hono/zod-openapi @hono/zod-validator zod @hono/node-server
pnpm add -D typescript @types/node tsxbunx create-hono <project-name>
# Select "bun" template
cd <project-name>
bun add @hono/zod-openapi @hono/zod-validator zodsrc/
index.ts # Entry point — runtime adapter + app mount
app.ts # Hono app factory — all routes and middleware
routes/
users.ts # User routes with OpenAPI schemas
health.ts # Health check
index.ts # Route aggregator
middleware/
auth.ts # Bearer auth / JWT middleware
logger.ts # Custom logger (or use built-in)
error-handler.ts # Global onError handler
schemas/
user.schema.ts # Zod schemas + OpenAPI route definitions
shared.schema.ts # Reusable schema parts (pagination, errors)
services/
users.service.ts # Business logic
types/
env.ts # Environment bindings type (Cloudflare or Node)
wrangler.toml # Cloudflare Workers config (if applicable)
.env.example # Required env vars templateapp works on Cloudflare Workers, Node.js, Bun, Deno, and AWS Lambda. The entry point adapts; the app code stays the same.@hono/zod-openapi for type-safe routes with automatic OpenAPI spec generation. This replaces raw app.get() for API routes.c) is the single argument to every handler — it provides c.req, c.json(), c.text(), c.env, c.var, etc.app.use() for global or per-route application.Env type for c.env (Cloudflare bindings) and c.var (middleware variables).any — leverage Hono's generics for full type inference across middleware and handlers.types/env.ts// For Cloudflare Workers:
export type Env = {
Bindings: {
DATABASE_URL: string;
JWT_SECRET: string;
MY_KV: KVNamespace;
};
Variables: {
user: { id: string; email: string };
};
};
// For Node.js:
// export type Env = {
// Variables: {
// user: { id: string; email: string };
// };
// };app.tsimport { OpenAPIHono } from '@hono/zod-openapi';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { prettyJSON } from 'hono/pretty-json';
import { userRoutes } from './routes/users.js';
import { healthRoutes } from './routes/health.js';
import type { Env } from './types/env.js';
export function createApp() {
const app = new OpenAPIHono<Env>();
// Global middleware
app.use('*', logger());
app.use('*', cors());
app.use('*', prettyJSON());
// Global error handler
app.onError((err, c) => {
console.error(err);
return c.json(
{ error: err.message || 'Internal Server Error' },
err instanceof Error && 'status' in err ? (err as any).status : 500,
);
});
// 404 handler
app.notFound((c) => {
return c.json({ error: 'Not found' }, 404);
});
// Mount routes
app.route('/api/users', userRoutes);
app.route('/api/health', healthRoutes);
// OpenAPI docs endpoint
app.doc('/api/doc', {
openapi: '3.1.0',
info: { title: 'API', version: '1.0.0' },
});
return app;
}index.tsimport { createApp } from './app.js';
const app = createApp();
export default app;index.tsimport { serve } from '@hono/node-server';
import { createApp } from './app.js';
const app = createApp();
serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server running on port ${info.port}`);
});index.tsimport { createApp } from './app.js';
const app = createApp();
export default {
port: 3000,
fetch: app.fetch,
};schemas/user.schema.tsimport { z } from 'zod';
import { createRoute } from '@hono/zod-openapi';
// Schemas
export const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string(),
createdAt: z.string().datetime(),
}).openapi('User');
export const CreateUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(1),
}).openapi('CreateUser');
export const UserParamsSchema = z.object({
id: z.string().uuid(),
}).openapi('UserParams');
export const ErrorSchema = z.object({
error: z.string(),
}).openapi('Error');
// Route definitions
export const createUserRoute = createRoute({
method: 'post',
path: '/',
tags: ['users'],
request: {
body: {
content: { 'application/json': { schema: CreateUserSchema } },
},
},
responses: {
201: {
content: { 'application/json': { schema: UserSchema } },
description: 'User created',
},
400: {
content: { 'application/json': { schema: ErrorSchema } },
description: 'Validation error',
},
},
});
export const getUserRoute = createRoute({
method: 'get',
path: '/:id',
tags: ['users'],
request: {
params: UserParamsSchema,
},
responses: {
200: {
content: { 'application/json': { schema: UserSchema } },
description: 'User found',
},
404: {
content: { 'application/json': { schema: ErrorSchema } },
description: 'User not found',
},
},
});routes/users.tsimport { OpenAPIHono } from '@hono/zod-openapi';
import { createUserRoute, getUserRoute } from '../schemas/user.schema.js';
import * as usersService from '../services/users.service.js';
import type { Env } from '../types/env.js';
export const userRoutes = new OpenAPIHono<Env>();
userRoutes.openapi(createUserRoute, async (c) => {
const body = c.req.valid('json');
const user = await usersService.create(body);
return c.json(user, 201);
});
userRoutes.openapi(getUserRoute, async (c) => {
const { id } = c.req.valid('param');
const user = await usersService.findById(id);
if (!user) {
return c.json({ error: `User ${id} not found` }, 404);
}
return c.json(user, 200);
});routes/health.tsimport { Hono } from 'hono';
import type { Env } from '../types/env.js';
export const healthRoutes = new Hono<Env>();
healthRoutes.get('/', (c) => {
return c.json({ status: 'ok', timestamp: new Date().toISOString() });
});services/users.service.tsimport { z } from 'zod';
import { CreateUserSchema, UserSchema } from '../schemas/user.schema.js';
type User = z.infer<typeof UserSchema>;
type CreateUserInput = z.infer<typeof CreateUserSchema>;
const users = new Map<string, User>();
export async function create(input: CreateUserInput): Promise<User> {
const user: User = {
id: crypto.randomUUID(),
email: input.email,
name: input.name,
createdAt: new Date().toISOString(),
};
users.set(user.id, user);
return user;
}
export async function findById(id: string): Promise<User | undefined> {
return users.get(id);
}middleware/auth.tsimport { createMiddleware } from 'hono/factory';
import { HTTPException } from 'hono/http-exception';
import type { Env } from '../types/env.js';
export const authMiddleware = createMiddleware<Env>(async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new HTTPException(401, { message: 'Missing bearer token' });
}
const token = authHeader.slice(7);
try {
// Replace with real JWT verification
const payload = JSON.parse(atob(token.split('.')[1]));
c.set('user', payload);
} catch {
throw new HTTPException(401, { message: 'Invalid token' });
}
await next();
});
// Usage:
// import { authMiddleware } from '../middleware/auth.js';
// app.use('/api/protected/*', authMiddleware);
// -- or per-route --
// app.get('/me', authMiddleware, (c) => c.json(c.var.user));middleware/validate.tsimport { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
// Usage in routes:
// app.post(
// '/items',
// zValidator('json', z.object({ name: z.string(), quantity: z.number().int().positive() })),
// (c) => {
// const body = c.req.valid('json'); // fully typed
// return c.json(body, 201);
// },
// );
//
// Validator targets: 'json' | 'query' | 'param' | 'header' | 'cookie' | 'form'// server side — export the app type
const routes = app
.get('/api/users/:id', async (c) => {
return c.json({ id: c.req.param('id'), name: 'Jane' });
})
.post('/api/users', zValidator('json', CreateUserSchema), async (c) => {
const body = c.req.valid('json');
return c.json({ id: '1', ...body }, 201);
});
export type AppType = typeof routes;
// client side — full type inference
// import { hc } from 'hono/client';
// import type { AppType } from '../server.js';
//
// const client = hc<AppType>('http://localhost:3000');
// const res = await client.api.users[':id'].$get({ param: { id: '1' } });
// const data = await res.json(); // typed as { id: string; name: string }import { HTTPException } from 'hono/http-exception';
// Throw anywhere in a handler or middleware — Hono catches it automatically
throw new HTTPException(404, { message: 'User not found' });
throw new HTTPException(403, { message: 'Forbidden' });
// Custom error response body
throw new HTTPException(422, {
res: new Response(JSON.stringify({ error: 'Validation failed', details: errors }), {
status: 422,
headers: { 'Content-Type': 'application/json' },
}),
});.env.example to .env and fill in valuespnpm install (or bun install)wrangler dev src/index.ts (Workers) / pnpm tsx watch src/index.ts (Node.js) / bun --watch src/index.ts (Bun)curl http://localhost:3000/api/health# Development — Cloudflare Workers
wrangler dev src/index.ts
# Development — Node.js
pnpm tsx watch src/index.ts
# Development — Bun
bun --watch src/index.ts
# Deploy — Cloudflare Workers
wrangler deploy
# Build — Node.js
pnpm tsup src/index.ts --format esm
# Type check
pnpm tsc --noEmit
# Test (vitest)
pnpm vitest run
# Generate OpenAPI spec to file
# (programmatic: fetch http://localhost:3000/api/doc and save)c.env.DB.c.env.MY_KV for Cloudflare KV, or the Cache API for edge caching.hc client provides end-to-end type safety between server and client without code generation. Ideal for monorepo setups./api/doc JSON endpoint.app.request() for in-process testing without a running server. Works with any test runner (vitest, jest, bun:test).c.streamText() and c.stream() for SSE and streaming responses natively.hono/serve-static for serving static assets (Node.js) or Workers Sites / Assets for Cloudflare.app.ts runtime-agnostic. Create separate entry files (index.worker.ts, index.node.ts, index.bun.ts) that import the same app and wire the runtime adapter.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.