From prototyping-skills
This skill generates or scaffolds a Hono API package using @hono/zod-openapi with Zod validation and OpenAPI spec generation. It should be used when the user asks to create an API, add API routes, build endpoints, scaffold a REST API, work on the api package, or mentions Hono, OpenAPI, or API development. It also applies when the user is working inside a packages/api directory or mentions exposing core functionality via HTTP.
npx claudepluginhub kjgarza/marketplace-claude --plugin prototyping-skillsThis skill is limited to using the following tools:
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Generate code for a Hono API that runs on Bun and uses @hono/zod-openapi for route-level schema definitions with automatic OpenAPI spec generation.
These are the team defaults. Multiple people work on these prototypes, so consistency matters. In execution mode, follow these defaults unless the project's CLAUDE.md explicitly overrides them.
In Plan mode, suggest alternatives using the deviation protocol from the
prototyping-skills:team-conventions skill: state the default, name the alternative,
explain the trade-off, flag the blast radius, let the human decide.
@hono/zod-openapi with createRoute() + app.openapi(). Not hono/validator directly, not raw app.get().https://jsonapi.org/format/). Resource-centric, plural nouns, relationships as nested paths.http, fs, etc. Use Bun.env not process.env.c), not Express-style (req, res, next).@repo/types. Never duplicate type definitions in the API package.@repo/core. API handlers are thin wrappers.bun:sqlite when persistence is needed. Not better-sqlite3, not Prisma, not Drizzle (unless deviation approved).packages/api/
├── src/
│ ├── index.ts # App entry point, mounts route groups
│ ├── routes/ # One file per resource/domain
│ │ ├── health.ts
│ │ └── [resource].ts
│ ├── middleware/ # Custom Hono middleware
│ └── lib/ # Shared utilities (error formatting, etc.)
├── package.json
└── tsconfig.json
package.json must include:
{
"type": "module",
"scripts": {
"dev": "bun --watch src/index.ts",
"start": "bun src/index.ts"
},
"dependencies": {
"hono": "^4",
"@hono/zod-openapi": "^0.18",
"zod": "^3",
"@repo/types": "workspace:*",
"@repo/core": "workspace:*"
}
}
Follow the JSON:API specification for URL design:
| Pattern | Example | Purpose |
|---|---|---|
/{resource} | /articles | Collection (plural nouns) |
/{resource}/{id} | /articles/1 | Individual resource |
/{resource}/{id}/{related} | /articles/1/comments | Related resources |
/{resource}/{id}/relationships/{relation} | /articles/1/relationships/author | Relationship linkage |
Rules:
/articles, /blog-posts (not /article, /blogPosts).POST /articles, not POST /articles/create)./articles/1/comments (not /authors/1/articles/2/comments).Every route MUST use createRoute + app.openapi(). This is non-negotiable.
import { createRoute, z } from "@hono/zod-openapi";
import { OpenAPIHono } from "@hono/zod-openapi";
const app = new OpenAPIHono();
const getItemRoute = createRoute({
method: "get",
path: "/items/{id}",
request: {
params: z.object({
id: z.string().openapi({ description: "Item ID", example: "abc-123" }),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.object({ id: z.string(), name: z.string() }),
},
},
description: "Item found",
},
404: {
content: {
"application/json": {
schema: z.object({ error: z.string() }),
},
},
description: "Item not found",
},
},
tags: ["Items"],
});
app.openapi(getItemRoute, async (c) => {
const { id } = c.req.valid("param");
// Call into @repo/core for business logic
return c.json({ id, name: "Example" }, 200);
});
const createItemRoute = createRoute({
method: "post",
path: "/items",
request: {
body: {
content: {
"application/json": {
schema: z.object({
name: z.string().min(1),
description: z.string().optional(),
}),
},
},
required: true,
},
},
responses: {
201: {
content: {
"application/json": {
schema: z.object({ id: z.string(), name: z.string() }),
},
},
description: "Created",
},
},
tags: ["Items"],
});
app.openapi(createItemRoute, async (c) => {
const body = c.req.valid("json");
return c.json({ id: "new-id", name: body.name }, 201);
});
// src/index.ts
import { OpenAPIHono } from "@hono/zod-openapi";
import { swaggerUI } from "@hono/swagger-ui";
import { cors } from "hono/cors";
import itemRoutes from "./routes/items";
import healthRoutes from "./routes/health";
const app = new OpenAPIHono();
app.use("/*", cors());
app.route("/api", itemRoutes);
app.route("/", healthRoutes);
app.doc("/doc", {
openapi: "3.1.0",
info: { title: "API", version: "0.1.0" },
});
app.get("/swagger", swaggerUI({ url: "/doc" }));
export default {
port: Bun.env.PORT ?? 3001,
fetch: app.fetch,
};
import { HTTPException } from "hono/http-exception";
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);
});
OpenAPIHono instance, mounted in index.ts via app.route().@repo/core, not in route handlers. Handlers: validate, call core, format response.schemas.ts if shared.@repo/types (add .openapi() refinements in the API package).Bun.env.VARIABLE_NAME, never process.env.