backend-node/hono-project-starter/SKILL.md
Scaffold a production-ready Hono 4.x API with TypeScript, Zod validation, OpenAPI generation, and multi-runtime support (Cloudflare Workers, Node.js, Bun).
npx skillsauth add achreftlili/deep-dev-skills hono-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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-types
pnpm 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 tsx
bunx create-hono <project-name>
# Select "bun" template
cd <project-name>
bun add @hono/zod-openapi @hono/zod-validator zod
src/
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 template
app 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.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.