CtrlK
BlogDocsLog inGet started
Tessl Logo

hono-project-starter

Scaffold a production-ready Hono 4.x API with TypeScript, Zod validation, OpenAPI generation, and multi-runtime support (Cloudflare Workers, Node.js, Bun).

90

Quality

87%

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Passed

No known issues

SKILL.md
Quality
Evals
Security

Hono Project Starter

Scaffold a production-ready Hono 4.x API with TypeScript, Zod validation, OpenAPI generation, and multi-runtime support (Cloudflare Workers, Node.js, Bun).

Prerequisites

  • Node.js >= 20.x / Bun >= 1.1 (depending on target runtime)
  • pnpm >= 9.x (or npm/bun)
  • TypeScript >= 5.3
  • Wrangler >= 3.x (if targeting Cloudflare Workers)

Scaffold Command

Cloudflare Workers (default)

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

Node.js

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

Bun

bunx create-hono <project-name>
# Select "bun" template
cd <project-name>
bun add @hono/zod-openapi @hono/zod-validator zod

Project Structure

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

Key Conventions

  • Hono is runtime-agnostic — the same app works on Cloudflare Workers, Node.js, Bun, Deno, and AWS Lambda. The entry point adapts; the app code stays the same.
  • Use @hono/zod-openapi for type-safe routes with automatic OpenAPI spec generation. This replaces raw app.get() for API routes.
  • Context (c) is the single argument to every handler — it provides c.req, c.json(), c.text(), c.env, c.var, etc.
  • Middleware is composable — chain with app.use() for global or per-route application.
  • Type-safe environment bindings — define Env type for c.env (Cloudflare bindings) and c.var (middleware variables).
  • No any — leverage Hono's generics for full type inference across middleware and handlers.

Essential Patterns

Environment Type — 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 Factory — app.ts

import { 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;
}

Entry — Cloudflare Workers — index.ts

import { createApp } from './app.js';

const app = createApp();

export default app;

Entry — Node.js — index.ts

import { 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}`);
});

Entry — Bun — index.ts

import { createApp } from './app.js';

const app = createApp();

export default {
  port: 3000,
  fetch: app.fetch,
};

Zod OpenAPI Schemas — schemas/user.schema.ts

import { 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 with OpenAPI — routes/users.ts

import { 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);
});

Health Route — routes/health.ts

import { 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() });
});

Service — services/users.service.ts

import { 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);
}

Auth Middleware — middleware/auth.ts

import { 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));

Standalone Zod Validator (non-OpenAPI routes) — middleware/validate.ts

import { 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'

RPC Mode — Type-Safe Client

// 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 }

Error Handling with HTTPException

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' },
  }),
});

First Steps After Scaffold

  1. Copy .env.example to .env and fill in values
  2. Install dependencies: pnpm install (or bun install)
  3. Start dev server: wrangler dev src/index.ts (Workers) / pnpm tsx watch src/index.ts (Node.js) / bun --watch src/index.ts (Bun)
  4. Test the API: curl http://localhost:3000/api/health

Common Commands

# 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)

Integration Notes

  • Database on Workers: Use Cloudflare D1 (SQLite), Hyperdrive (proxy to Postgres), or Turso (libSQL). Access via c.env.DB.
  • Database on Node/Bun: Use Prisma, Drizzle, or any Node-compatible ORM. Import in services.
  • KV/Cache on Workers: Use c.env.MY_KV for Cloudflare KV, or the Cache API for edge caching.
  • RPC Client: Hono's hc client provides end-to-end type safety between server and client without code generation. Ideal for monorepo setups.
  • OpenAPI UI: Serve Swagger UI or Scalar at a static route, pointing to the /api/doc JSON endpoint.
  • Testing: Use app.request() for in-process testing without a running server. Works with any test runner (vitest, jest, bun:test).
  • Streaming: Hono supports c.streamText() and c.stream() for SSE and streaming responses natively.
  • Static Files: Use hono/serve-static for serving static assets (Node.js) or Workers Sites / Assets for Cloudflare.
  • Multi-runtime deploy: Keep 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.
Repository
achreftlili/deep-dev-skills
Last updated
Created

Is this your skill?

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.