api-framework-hono · git:20260202.b236384 · 2026-02-02 · sha256 99b911e77b1a3461
api-framework-hono git:20260202.b236384A
Immutable. This exact content is served forever at /api/v1/blob/99b911e77b1a3461.
---
name: api-framework-hono
description: Hono routes, OpenAPI, Zod validation
---
# API Development with Hono + OpenAPI
> **Quick Guide:** Use Hono with @hono/zod-openapi for type-safe REST APIs that auto-generate OpenAPI specs. Zod schemas provide validation AND documentation. Export your Hono app for spec generation to enable client code generation.
---
<critical_requirements>
## CRITICAL: Before Using This Skill
> **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
**(You MUST call `extendZodWithOpenApi(z)` BEFORE defining ANY Zod schemas)**
**(You MUST export the `app` instance for OpenAPI spec generation)**
**(You MUST include `operationId` in every route for clean client generation)**
</critical_requirements>
---
**Auto-detection:** Hono, @hono/zod-openapi, OpenAPIHono, createRoute, Zod schemas with .openapi(), app.route(), createMiddleware, rate limiting, CORS configuration, health checks, hc client, RPC mode, getContext, tryGetContext, contextStorage, some/every/except middleware
**When to use:**
- Building type-safe REST APIs in Next.js API routes with Hono
- Defining OpenAPI specifications with automatic validation
- Creating standardized error responses with proper status codes
- Implementing filtering, pagination, and sorting patterns
- Need public or multi-client API with documentation
- Production APIs requiring rate limiting, CORS, health checks
**When NOT to use:**
- Simple CRUD operations with no external consumers (use Server Actions instead)
- Internal-only APIs without documentation requirements (simpler approaches exist)
- Forms that don't need complex validation (React Hook Form + Server Actions)
- When building GraphQL APIs (use Apollo Server or similar)
- Single-use endpoints with no schema reuse (over-engineering)
**Key patterns covered:**
- Hono API route setup with OpenAPI integration
- Zod schema definition with OpenAPI metadata (.openapi() method)
- Route definition with createRoute (operationId, tags, responses)
- Standardized error handling with error codes and constants
- Filtering with multiple values (comma-separated, case-insensitive)
- Pagination patterns (offset-based with proper constants)
- Data transformation utilities (date formatting, object mapping)
- OpenAPI spec generation at build time
- Authentication middleware with type-safe variables
- Rate limiting with response headers and 429 handling
- CORS configuration with origin allowlisting
- Health check endpoints (shallow and deep)
- Request/response logging with PII sanitization
- Caching strategies (Cache-Control, ETags)
- RPC client (hc) with end-to-end type safety (v4.11.0+)
- Context Storage for accessing context outside handlers (v4.6.0+)
- Combine Middleware (some/every/except) for complex auth logic (v4.x)
**Detailed Resources:**
- For code examples, see [examples/](examples/) (core.md, validation.md, routes.md, middleware.md, etc.)
- For decision frameworks and anti-patterns, see [reference.md](reference.md)
---
<philosophy>
## Philosophy
**Type safety + documentation from code.** Zod schemas serve both validation AND OpenAPI spec generation. Single source of truth flows to frontend via your chosen client code generator.
**Use Hono + OpenAPI when:** Building public/multi-client APIs, need auto-generated documentation, require formal OpenAPI specs.
**Use Server Actions when:** Simple CRUD, internal-only, no external API consumers, no complex validation.
</philosophy>
---
<patterns>
## Core Patterns
### Pattern 1: API Route Setup
Structure API routes in `/app/api/` using catch-all route pattern. Use `app.route()` for modularization.
**File: `/app/api/[[...route]]/route.ts`**
```typescript
// Import order: External deps -> Relative imports
import { OpenAPIHono } from "@hono/zod-openapi";
import { handle } from "hono/vercel";
import { jobsRoutes } from "../routes/jobs";
import { companiesRoutes } from "../routes/companies";
// Create main app with base path
const app = new OpenAPIHono().basePath("/api");
// Mount route modules using app.route()
app.route("/", jobsRoutes);
app.route("/", companiesRoutes);
// REQUIRED: Export app for OpenAPI spec generation
export { app };
// Export handlers for Next.js (all HTTP methods)
export const GET = handle(app);
export const POST = handle(app);
export const PUT = handle(app);
export const PATCH = handle(app);
export const DELETE = handle(app);
```
**Why good:** app.route() prevents God files, app export enables build-time spec generation, named exports follow project convention
---
### Pattern 2: Zod Schema Definition with OpenAPI
Schemas serve both validation AND documentation via `.openapi()` method.
**File: `/app/api/schemas.ts`**
```typescript
import { z } from "zod";
import { extendZodWithOpenApi } from "@hono/zod-openapi";
// REQUIRED: Extend Zod with OpenAPI methods BEFORE defining schemas
extendZodWithOpenApi(z);
const MIN_SALARY = 0;
const CURRENCY_CODE_LENGTH = 3;
const MIN_TITLE_LENGTH = 1;
const MAX_TITLE_LENGTH = 255;
const DEFAULT_LIMIT = "50";
export const SalarySchema = z
.object({
min: z.number().min(MIN_SALARY),
max: z.number().min(MIN_SALARY),
currency: z.string().length(CURRENCY_CODE_LENGTH),
})
.openapi("Salary", {
example: { min: 60000, max: 90000, currency: "EUR" },
});
export const JobSchema = z
.object({
id: z.string().uuid(),
title: z.string().min(MIN_TITLE_LENGTH).max(MAX_TITLE_LENGTH),
description: z.string(),
employmentType: z.string().nullable(),
salary: SalarySchema.nullable(),
})
.openapi("Job");
export type Job = z.infer<typeof JobSchema>;
```
**Why good:** extendZodWithOpenApi first (required for .openapi() to exist), named constants prevent magic number bugs, reusable sub-schemas reduce duplication
---
### Pattern 3: Route Definition with createRoute
Define routes with OpenAPI metadata using `createRoute` and `app.openapi()`.
**File: `/app/api/routes/jobs.ts`**
```typescript
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
import { and, eq, desc, isNull } from "drizzle-orm";
import { db, jobs, companies } from "@/lib/db";
import {
JobsQuerySchema,
JobsResponseSchema,
ErrorResponseSchema,
} from "../schemas";
const DEFAULT_QUERY_LIMIT = 100;
const app = new OpenAPIHono();
const getJobsRoute = createRoute({
method: "get",
path: "/jobs",
operationId: "getJobs", // Used for generated client method names
tags: ["Jobs"], // Groups endpoints in documentation
summary: "Get all jobs",
description: "Retrieve active job postings with optional filters",
request: {
query: JobsQuerySchema,
},
responses: {
200: {
description: "List of jobs",
content: { "application/json": { schema: JobsResponseSchema } },
},
500: {
description: "Internal server error",
content: { "application/json": { schema: ErrorResponseSchema } },
},
},
});
app.openapi(getJobsRoute, async (c) => {
try {
const { country, employment_type } = c.req.valid("query");
const conditions = [eq(jobs.isActive, true), isNull(jobs.deletedAt)];
if (country) {
conditions.push(eq(jobs.country, country));
}
const results = await db
.select()
.from(jobs)
.where(and(...conditions))
.orderBy(desc(jobs.createdAt))
.limit(DEFAULT_QUERY_LIMIT);
return c.json({ jobs: results, total: results.length }, 200);
} catch (error) {
console.error("Error fetching jobs:", error);
return c.json(
{
error: "Failed to fetch jobs",
message: error instanceof Error ? error.message : "Unknown error",
},
500,
);
}
});
// Named export (project convention - no default exports)
export { app as jobsRoutes };
```
**Why good:** operationId becomes client method name (getJobs vs get_api_jobs), c.req.valid() enforces schema validation, soft delete checks prevent exposing deleted data
---
### Pattern 4: Standardized Error Handling
Consistent error responses with named error codes across all routes.
```typescript
import { z } from "zod";
import type { Context } from "hono";
const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
const HTTP_STATUS_CONFLICT = 409;
const HTTP_STATUS_INTERNAL_ERROR = 500;
export const ErrorCodes = {
VALIDATION_ERROR: "validation_error",
NOT_FOUND: "not_found",
UNAUTHORIZED: "unauthorized",
FORBIDDEN: "forbidden",
INTERNAL_ERROR: "internal_error",
DATABASE_ERROR: "database_error",
} as const;
export const handleRouteError = (error: unknown, c: Context) => {
console.error("Route error:", error);
if (error instanceof z.ZodError) {
return c.json(
{
error: ErrorCodes.VALIDATION_ERROR,
message: "Validation failed",
statusCode: HTTP_STATUS_UNPROCESSABLE_ENTITY,
details: error.errors,
},
HTTP_STATUS_UNPROCESSABLE_ENTITY,
);
}
if (error instanceof Error) {
if (error.message.includes("unique constraint")) {
return c.json(
{
error: ErrorCodes.VALIDATION_ERROR,
message: "Resource already exists",
statusCode: HTTP_STATUS_CONFLICT,
},
HTTP_STATUS_CONFLICT,
);
}
return c.json(
{
error: ErrorCodes.INTERNAL_ERROR,
message: error.message,
statusCode: HTTP_STATUS_INTERNAL_ERROR,
},
HTTP_STATUS_INTERNAL_ERROR,
);
}
return c.json(
{
error: ErrorCodes.INTERNAL_ERROR,
message: "An unexpected error occurred",
statusCode: HTTP_STATUS_INTERNAL_ERROR,
},
HTTP_STATUS_INTERNAL_ERROR,
);
};
```
**Why good:** Named error codes enable frontend handling (switch on code), Zod error details show which field failed, consistent shape = predictable client parsing
</patterns>
---
<critical_reminders>
## CRITICAL REMINDERS
**Before you implement ANY Hono API route, verify these requirements are met:**
> **All code must follow project conventions in CLAUDE.md**
**(You MUST call `extendZodWithOpenApi(z)` BEFORE defining ANY Zod schemas)**
**(You MUST export the `app` instance for OpenAPI spec generation)**
**(You MUST include `operationId` in every route for clean client generation)**
**Failure to follow these rules will break OpenAPI spec generation.**
</critical_reminders>