api-framework-hono
Hono routes, OpenAPI, Zod validation
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-framework-hono/skills/api-framework-hono
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
API Development with Hono + OpenAPI
Quick Guide: Use Hono with
@hono/zod-openapifor type-safe REST APIs that auto-generate OpenAPI specs. Importzfrom@hono/zod-openapi(NOT fromzod) so.openapi()is available on all schemas. Always includeoperationIdin routes and export theappinstance for spec 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 import z from @hono/zod-openapi, NOT from zod -- this gives Zod the .openapi() method)
(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 with auto-generated OpenAPI specs
- Defining OpenAPI specifications with automatic Zod validation
- Creating standardized error responses with proper status codes
- Implementing filtering, pagination, and sorting patterns
- Public or multi-client APIs needing formal documentation
- Production APIs requiring rate limiting, CORS, health checks
When NOT to use:
- Simple CRUD with no external consumers (framework-native endpoints are simpler)
- Internal-only APIs without documentation requirements
- Single-use endpoints with no schema reuse (over-engineering)
Key patterns covered:
- Modular route setup with
app.route()andOpenAPIHono - Zod schema definitions with
.openapi()metadata - Route definition with
createRoute(operationId, tags, responses) - Error handling with named error codes
- Filtering, pagination, and data transformation
- Auth, rate limiting, CORS, logging, caching middleware
- Health check endpoints (shallow and deep)
- RPC client (
hc) with end-to-end type safety - Context Storage for out-of-handler context access
- Combine Middleware (
some/every/except) for declarative auth
Detailed Resources:
- examples/core.md - Route setup, list/detail endpoints
- examples/validation.md - Zod schema definitions with OpenAPI
- examples/routes.md - Filtering, pagination, data transformation
- examples/middleware.md - Auth, rate limiting, CORS, logging, caching
- examples/error-handling.md - Standardized error responses
- examples/openapi.md - Spec generation (build-time and endpoint)
- examples/health-checks.md - Liveness and readiness checks
- examples/advanced-v4.md - RPC, Context Storage, Combine Middleware
- reference.md - Decision frameworks, anti-patterns, production checklist
<red_flags>
RED FLAGS
High Priority:
- Importing
zfrom"zod"instead of"@hono/zod-openapi"--.openapi()won't be available - Missing
operationIdin routes -- generated client has ugly method names - Not exporting
appinstance -- can't generate OpenAPI spec at build time - JWT/JWK without explicit
algoption -- algorithm confusion vulnerability (CVE-2026-22817/22818)
Medium Priority:
- Using
c.req.param()/c.req.query()instead ofc.req.valid()-- bypasses Zod validation - No pagination limits on list endpoints -- returns massive datasets
- Generating spec at runtime instead of build time -- wasted CPU per request
- Not returning proper status codes -- always specify (200, 404, 500)
- Wildcard CORS (
"*") withcredentials: true-- browsers reject this (spec violation)
Gotchas & Edge Cases:
c.req.valid("param")uses singular"param", not"params"-- easy to mistype- In-memory rate limiting doesn't work across multiple instances -- use a shared store
- CORS middleware must be registered before auth middleware -- OPTIONS preflight bypasses auth
- ETags should not be used for user-specific data (generates unique ETag per user)
- RPC routes must be chained (
.openapi(r1, h1).openapi(r2, h2)) for type inference -- separate calls break it - Both client and server
tsconfig.jsonneed"strict": truefor RPC type inference contextStorage()middleware must be registered before any code callsgetContext()- Use
tryGetContext()(v4.11.0+) in code that may run outside request context (tests, background jobs) - Middleware
next()never throws in Hono -- wrappingawait next()in try/catch is unnecessary getConnInfois adapter-specific -- import fromhono/bun,hono/deno,@hono/node-server/conninfo, etc. (NOT fromhono/ip-restriction)
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST import z from @hono/zod-openapi, NOT from zod -- this gives Zod the .openapi() method)
(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 and type safety.
</critical_reminders>
Files (skills)
-
examples
-
advanced-v4.md 12.7 KB
# Hono + OpenAPI - Advanced v4.x Features > RPC client, Context Storage, Combine Middleware, and other v4.x features. See [core.md](core.md) for basic patterns. **Prerequisites**: Understand route definition patterns from core examples first. --- ## Pattern 1: RPC Client (hc) with End-to-End Type Safety **Since Hono v4.x** - Share type-safe API specifications between server and client without code generation. ### Server Setup ```typescript import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; const app = new OpenAPIHono().basePath("/api"); // Chain route definitions for proper type inference const routes = app .openapi( createRoute({ method: "get", path: "/users/{id}", operationId: "getUserById", request: { params: z.object({ id: z.string() }), }, responses: { 200: { content: { "application/json": { schema: z.object({ id: z.string(), name: z.string() }), }, }, description: "User details", }, }, }), async (c) => { const { id } = c.req.valid("param"); return c.json({ id, name: "John Doe" }, 200); }, ) .openapi( createRoute({ method: "post", path: "/users", operationId: "createUser", request: { body: { content: { "application/json": { schema: z.object({ name: z.string() }) }, }, }, }, responses: { 201: { content: { "application/json": { schema: z.object({ id: z.string(), name: z.string() }), }, }, description: "Created user", }, }, }), async (c) => { const { name } = c.req.valid("json"); return c.json({ id: crypto.randomUUID(), name }, 201); }, ); // REQUIRED: Export app type for RPC client export type AppType = typeof routes; // Named export for spec generation export { app }; // Export HTTP handlers via your framework adapter (hono/vercel, hono/bun, etc.) ``` **Why good:** Chained route definitions enable full type inference, AppType export shares types without code generation **TypeScript Requirement:** Both client and server `tsconfig.json` must have `"strict": true` in `compilerOptions` for proper type inference in monorepos. ### Client Usage ```typescript // /lib/api-client.ts import { hc } from "hono/client"; import type { AppType } from "@/app/api/[[...route]]/route"; import type { InferRequestType, InferResponseType } from "hono/client"; const API_BASE_URL = process.env.API_BASE_URL || "http://localhost:3000"; // Create type-safe client export const apiClient = hc<AppType>(API_BASE_URL); // Type-safe request/response types (for use with data fetching libraries) type GetUserRequest = InferRequestType< (typeof apiClient.api.users)[":id"]["$get"] >; type GetUserResponse = InferResponseType< (typeof apiClient.api.users)[":id"]["$get"] >; // Usage in component async function fetchUser(id: string) { const res = await apiClient.api.users[":id"].$get({ param: { id }, }); if (!res.ok) { throw new Error(`Failed to fetch user: ${res.status}`); } // Type-safe response const user = await res.json(); return user; // type: { id: string; name: string } } // Create user with type-safe body async function createUser(name: string) { const res = await apiClient.api.users.$post({ json: { name }, }); if (!res.ok) { throw new Error(`Failed to create user: ${res.status}`); } return res.json(); } ``` **Why good:** Full type safety without code generation, `InferRequestType`/`InferResponseType` enable type extraction for your data fetching library ### Typed URL Feature (v4.11.0+) ```typescript import { hc } from "hono/client"; import type { AppType } from "@/app/api/[[...route]]/route"; const API_BASE_URL = "http://localhost:3000"; // Pass base URL as second type parameter for precise URL types const client = hc<AppType, typeof API_BASE_URL>(API_BASE_URL); // URL is now precisely typed for use as cache keys const url = client.api.users[":id"].$url({ param: { id: "123" } }); // Type: URL with precise path type for caching libraries ``` **Why good:** Enables type-safe URL keys for caching and data fetching libraries --- ## Pattern 2: Context Storage (v4.6.0+) **Access context outside of handlers** - Useful for accessing Cloudflare Workers bindings, typed variables, etc. ### Basic Setup ```typescript // /app/api/[[...route]]/route.ts import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; import { contextStorage, getContext } from "hono/context-storage"; type Env = { Variables: { userId: string; requestId: string; }; Bindings: { DATABASE: D1Database; // Cloudflare D1 KV: KVNamespace; // Cloudflare KV }; }; const app = new OpenAPIHono<Env>(); // Enable context storage middleware app.use(contextStorage()); // Set variables in another middleware app.use("*", async (c, next) => { c.set("requestId", crypto.randomUUID()); await next(); }); ``` **Why good:** contextStorage() enables getContext() access anywhere in the call stack ### Accessing Context Outside Handlers ```typescript // /lib/database.ts import { getContext } from "hono/context-storage"; type Env = { Bindings: { DATABASE: D1Database }; Variables: { requestId: string }; }; // Access context in utility functions (outside route handlers) export async function getUser(id: string) { const ctx = getContext<Env>(); // Access Cloudflare bindings const db = ctx.env.DATABASE; // Access typed variables const requestId = ctx.var.requestId; console.log(`[${requestId}] Fetching user ${id}`); return db.prepare("SELECT * FROM users WHERE id = ?").bind(id).first(); } ``` **Why good:** getContext() provides typed access to bindings and variables in any function ### tryGetContext (v4.11.0+) ```typescript import { tryGetContext } from "hono/context-storage"; type Env = { Variables: { userId: string }; }; // Returns undefined instead of throwing when context unavailable export function getOptionalUserId(): string | undefined { const ctx = tryGetContext<Env>(); // Safe to use without try/catch return ctx?.var.userId; } // Use in code that may run outside request context export function logWithContext(message: string) { const ctx = tryGetContext<Env>(); if (ctx) { console.log(`[User: ${ctx.var.userId}] ${message}`); } else { console.log(message); } } ``` **Why good:** tryGetContext() is safer for code paths that may run outside request context (tests, background jobs) --- ## Pattern 3: Combine Middleware (some/every/except) **Complex middleware composition** - Build sophisticated access control with `some`, `every`, and `except`. ### some - First Successful Middleware Wins ```typescript import { some } from "hono/combine"; import { bearerAuth } from "hono/bearer-auth"; const VALID_API_KEY = process.env.API_KEY!; const RATE_LIMIT_WINDOW_MS = 60000; const MAX_REQUESTS = 100; // Skip rate limiting if client has valid token app.use( "/api/*", some( // If bearer auth succeeds, rate limiting is skipped bearerAuth({ token: VALID_API_KEY }), // Otherwise, apply rate limiting rateLimitMiddleware, ), ); ``` **Why good:** Premium API key holders skip rate limits, anonymous users get rate limited ### every - All Middleware Must Succeed ```typescript import { every } from "hono/combine"; import { bearerAuth } from "hono/bearer-auth"; import { ipRestriction } from "hono/ip-restriction"; // getConnInfo is adapter-specific — import from your runtime: // import { getConnInfo } from "hono/bun"; // import { getConnInfo } from "hono/deno"; // import { getConnInfo } from "@hono/node-server/conninfo"; // import { getConnInfo } from "hono/cloudflare-workers"; import { getConnInfo } from "hono/bun"; const ADMIN_API_KEY = process.env.ADMIN_API_KEY!; const ALLOWED_ADMIN_IPS = ["192.168.1.100", "10.0.0.1"]; // Both IP restriction AND bearer auth must pass app.use( "/api/admin/*", every( ipRestriction(getConnInfo, { allowList: ALLOWED_ADMIN_IPS }), bearerAuth({ token: ADMIN_API_KEY }), ), ); ``` **Why good:** Defense in depth - both network location AND credentials required for admin routes ### except - Apply Middleware to All Except Specific Paths ```typescript import { except } from "hono/combine"; import { bearerAuth } from "hono/bearer-auth"; const API_KEY = process.env.API_KEY!; // Apply auth to all API routes EXCEPT public endpoints app.use( "/api/*", except( // Paths to exclude from auth ["/api/health", "/api/public/*", "/api/docs"], // Middleware to apply to everything else bearerAuth({ token: API_KEY }), ), ); ``` **Why good:** Clean exception handling without duplicating middleware registration ### Complex Access Control ```typescript import { some, every, except } from "hono/combine"; import { bearerAuth } from "hono/bearer-auth"; import { ipRestriction } from "hono/ip-restriction"; import { getConnInfo } from "hono/bun"; // adapter-specific — see note above const INTERNAL_TOKEN = process.env.INTERNAL_TOKEN!; const EXTERNAL_TOKEN = process.env.EXTERNAL_TOKEN!; const INTERNAL_IPS = ["10.0.0.0/8", "192.168.0.0/16"]; // Complex rule: Allow access if EITHER: // 1. Request is from internal network WITH valid internal token, OR // 2. Request has valid external token (from anywhere) app.use( "/api/*", except( ["/api/health", "/api/docs"], some( every( ipRestriction(getConnInfo, { allowList: INTERNAL_IPS }), bearerAuth({ token: INTERNAL_TOKEN }), ), bearerAuth({ token: EXTERNAL_TOKEN }), ), ), ); ``` **Why good:** Composable middleware rules replace complex if/else auth logic --- ## Pattern 4: Custom NotFoundResponse Type (v4.11.0+) **Type-safe 404 responses** - Module augmentation for custom not found response typing. ```typescript // /types/hono.d.ts import { OpenAPIHono } from "@hono/zod-openapi"; // Augment the Hono module to type 404 responses declare module "hono" { interface NotFoundResponse { error: "not_found"; message: string; path: string; } } ``` ```typescript // /app/api/[[...route]]/route.ts import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; const app = new OpenAPIHono(); // Now c.notFound() is typed app.notFound((c) => { return c.json( { error: "not_found" as const, message: "Resource not found", path: c.req.path, }, 404, ); }); export type AppType = typeof app; ``` ```typescript // Client gets typed 404 response const res = await client.api.users[":id"].$get({ param: { id: "unknown" } }); if (res.status === 404) { const error = await res.json(); // Type: { error: "not_found"; message: string; path: string } console.log(error.path); } ``` **Why good:** 404 responses are properly typed on the client side via module augmentation --- ## Pattern 5: Timing Utility (wrapTime - v4.11.0+) **Simplified timing measurement** - Wrap a Promise with automatic timing. ```typescript import { timing, wrapTime } from "hono/timing"; const DEFAULT_QUERY_LIMIT = 100; const app = new OpenAPIHono(); // Enable timing headers app.use(timing()); app.openapi(getJobsRoute, async (c) => { // wrapTime(context, metricName, promise) - wraps a Promise with timing const jobs = await wrapTime( c, "db", fetchJobs({ limit: DEFAULT_QUERY_LIMIT }), ); return c.json({ jobs }, 200); }); ``` Response headers will include: ``` Server-Timing: db;dur=45.2 ``` **Why good:** Cleaner syntax than manual `startTime`/`endTime` pattern, automatic `Server-Timing` header generation --- ## Bad Example - Missing v4.x Features ```typescript // BAD Example - Not using v4.x features import { Hono } from "hono"; const app = new Hono(); // BAD: No AppType export for RPC // BAD: Not chaining routes for type inference app.get("/users/:id", async (c) => { const id = c.req.param("id"); return c.json({ id, name: "John" }); }); // BAD: Complex auth logic without combine middleware app.use("/api/*", async (c, next) => { const token = c.req.header("Authorization"); const ip = c.req.header("X-Forwarded-For"); // BAD: Messy if/else auth logic if (token === process.env.ADMIN_TOKEN) { await next(); } else if (INTERNAL_IPS.includes(ip || "")) { if (token === process.env.INTERNAL_TOKEN) { await next(); } else { return c.json({ error: "Unauthorized" }, 401); } } else { return c.json({ error: "Unauthorized" }, 401); } }); // BAD: Props drilling context to every utility function app.get("/users", async (c) => { // BAD: Have to pass c to every utility function const users = await fetchUsers(c.get("requestId")); return c.json(users); }); export default app; ``` **Why bad:** No RPC type safety, messy auth logic (use some/every/except), no context storage (prop drilling), default export breaks spec generation --- -
core.md 6.1 KB
# Hono + OpenAPI - Core Examples > Essential patterns for Hono with OpenAPI. See [SKILL.md](../SKILL.md) for core concepts and [reference.md](../reference.md) for decision frameworks. **Additional Examples:** - [validation.md](validation.md) - Zod schema definitions and OpenAPI integration - [routes.md](routes.md) - Filtering, pagination, and data transformation - [middleware.md](middleware.md) - Auth, rate limiting, CORS, logging, caching - [error-handling.md](error-handling.md) - Standardized error responses - [openapi.md](openapi.md) - OpenAPI spec generation - [health-checks.md](health-checks.md) - Health check endpoints - [advanced-v4.md](advanced-v4.md) - RPC client, Context Storage, Combine Middleware (v4.x) --- ## Pattern 1: Modular Route Setup ### Good Example - Modular Route Setup ```typescript // Import order: External deps -> Relative imports import { OpenAPIHono } from "@hono/zod-openapi"; 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 HTTP method handlers for your framework adapter // (e.g., hono/vercel, hono/cloudflare-workers, hono/bun, etc.) ``` **Why good:** `app.route()` prevents God files, app export enables build-time spec generation, named exports follow project convention ### Bad Example - Missing exports and poor structure ```typescript // BAD Example - Missing exports and poor structure import { Hono } from "hono"; const app = new Hono(); // BAD: Inline route definitions make file huge app.get("/jobs", async (c) => { // 100+ lines of code here... }); app.get("/companies", async (c) => { // 100+ lines of code here... }); // BAD: Default export prevents spec generation export default app; ``` **Why bad:** No OpenAPI means no docs/validation, inline routes create 1000+ line files, default export breaks spec generation, no modularization = unmaintainable --- ## Pattern 2: List Endpoint ### Good Example - List Endpoint with OpenAPI ```typescript import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; 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 { // Type-safe query parameter extraction const { country, employment_type } = c.req.valid("query"); // Use your database solution to query with filters const results = await fetchJobs({ country, employment_type, 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 with full types, consistent error shape --- ## Pattern 3: Detail Endpoint ### Good Example - Detail Endpoint with Path Params ```typescript const getJobByIdRoute = createRoute({ method: "get", path: "/jobs/{id}", operationId: "getJobById", tags: ["Jobs"], summary: "Get job by ID", request: { params: z.object({ id: z .string() .uuid() .openapi({ param: { name: "id", in: "path" }, example: "550e8400-e29b-41d4-a716-446655440000", }), }), }, responses: { 200: { description: "Job details", content: { "application/json": { schema: JobSchema } }, }, 404: { description: "Job not found", content: { "application/json": { schema: ErrorResponseSchema } }, }, 500: { description: "Internal server error", content: { "application/json": { schema: ErrorResponseSchema } }, }, }, }); app.openapi(getJobByIdRoute, async (c) => { try { // Type-safe param extraction (note: "param" not "params") const { id } = c.req.valid("param"); // Use your database solution to find by ID const job = await findJobById(id); if (!job) { return c.json( { error: "Job not found", message: `Job with ID ${id} does not exist` }, 404, ); } return c.json(job, 200); } catch (error) { console.error("Error fetching job:", error); return c.json( { error: "Failed to fetch job", message: error instanceof Error ? error.message : "Unknown error", }, 500, ); } }); ``` ### Bad Example - Poor practices ```typescript // BAD Example - Poor practices import { Hono } from "hono"; const app = new Hono(); app.get("/jobs", async (c) => { // BAD: No createRoute - no OpenAPI documentation // BAD: No type-safe query validation const country = c.req.query("country"); // BAD: Magic number limit const results = await fetchJobs({ country, limit: 100 }); // BAD: No error handling return c.json({ jobs: results }); }); export default app; // BAD: Default export ``` **Why bad:** No `createRoute` = no OpenAPI docs, no `c.req.valid()` = no validation, magic number limit, no error handling = 500s with no context, default export breaks spec generation --- -
error-handling.md 3.1 KB
# Hono + OpenAPI - Error Handling Examples > Standardized error response patterns. See [core.md](core.md) for route setup patterns. **Prerequisites**: Understand route handler patterns from core examples first. --- ## Pattern 1: Standardized Error Handling ### Good Example - Standardized Error Handling ```typescript import { z } from "@hono/zod-openapi"; import type { Context } from "hono"; const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422; const HTTP_STATUS_CONFLICT = 409; const HTTP_STATUS_INTERNAL_ERROR = 500; export const ErrorResponseSchema = z .object({ error: z.string(), message: z.string(), statusCode: z.number(), details: z.any().optional(), }) .openapi("ErrorResponse"); // Named constants for error codes (no magic strings) 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) => { // Always log with context console.error("Route error:", error); // Handle Zod validation errors 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, ); } // Handle database constraint violations 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, ); } // Fallback for unknown errors 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 **Usage in routes:** ```typescript app.openapi(getJobsRoute, async (c) => { try { // ... route logic } catch (error) { return handleRouteError(error, c); } }); ``` ### Bad Example - Inconsistent error handling ```typescript // BAD Example - Inconsistent error handling app.get("/jobs", async (c) => { try { const jobs = await fetchJobs(); return c.json(jobs); } catch (error) { // BAD: Magic number 500 // BAD: No error code constant // BAD: Generic message return c.json({ error: "Error" }, 500); } }); ``` **Why bad:** Magic 500 breaks when status changes, generic "Error" message can't be handled by frontend, no logging = blind to production issues --- -
health-checks.md 4.1 KB
# Hono + OpenAPI - Health Check Examples > Health check endpoint patterns for load balancers and monitoring. See [core.md](core.md) for route setup patterns. **Prerequisites**: Understand route definition patterns from core examples first. --- ## Pattern 1: Shallow and Deep Health Checks ### Good Example - Shallow and Deep Health Checks ```typescript import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; // Define health check result types as needed for your project const HTTP_STATUS_OK = 200; const HTTP_STATUS_SERVICE_UNAVAILABLE = 503; const HEALTH_CHECK_TIMEOUT_MS = 5000; const HealthStatusSchema = z .object({ status: z.enum(["healthy", "unhealthy"]), timestamp: z.string(), uptime: z.number(), dependencies: z .object({ database: z.enum(["connected", "disconnected", "degraded"]), redis: z.enum(["connected", "disconnected", "degraded"]).optional(), }) .optional(), }) .openapi("HealthStatus"); // Shallow health check (fast, no dependency checks) const healthRoute = createRoute({ method: "get", path: "/health", operationId: "getHealth", tags: ["Health"], summary: "Health check", description: "Lightweight health check for load balancers", responses: { 200: { description: "Service is healthy", content: { "application/json": { schema: HealthStatusSchema } }, }, }, }); app.openapi(healthRoute, async (c) => { return c.json( { status: "healthy", timestamp: new Date().toISOString(), uptime: process.uptime(), }, HTTP_STATUS_OK, ); }); // Deep health check (includes dependency checks) const healthDeepRoute = createRoute({ method: "get", path: "/health/deep", operationId: "getHealthDeep", tags: ["Health"], summary: "Deep health check", description: "Comprehensive health check including dependencies", responses: { 200: { description: "Service and dependencies are healthy", content: { "application/json": { schema: HealthStatusSchema } }, }, 503: { description: "Service or dependencies are unhealthy", content: { "application/json": { schema: HealthStatusSchema } }, }, }, }); app.openapi(healthDeepRoute, async (c) => { const checks = await Promise.allSettled([checkDatabase(), checkRedis()]); const dbStatus = checks[0].status === "fulfilled" ? checks[0].value.status : "disconnected"; const redisStatus = checks[1].status === "fulfilled" ? checks[1].value.status : "disconnected"; const isHealthy = dbStatus === "connected" && redisStatus === "connected"; return c.json( { status: isHealthy ? "healthy" : "unhealthy", timestamp: new Date().toISOString(), uptime: process.uptime(), dependencies: { database: dbStatus, redis: redisStatus, }, }, isHealthy ? HTTP_STATUS_OK : HTTP_STATUS_SERVICE_UNAVAILABLE, ); }); // Dependency check helper (with timeout) const checkDatabase = async (): Promise<{ status: "connected" | "disconnected" | "degraded"; }> => { try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout")), HEALTH_CHECK_TIMEOUT_MS), ); // Use your database client's ping/health check method await Promise.race([checkDatabaseConnection(), timeoutPromise]); return { status: "connected" }; } catch (error) { console.error("Database health check failed:", error); return { status: "disconnected" }; } }; ``` **Why good:** Shallow `/health` is fast (liveness for load balancers), deep `/health/deep` checks deps (readiness), timeout prevents hanging forever, 503 tells orchestrators to restart ### Bad Example - Slow health check ```typescript // BAD Example - Slow health check app.get("/health", async (c) => { // BAD: Checks dependencies on every request (slow for load balancers) const db = await checkDatabase(); const redis = await checkRedis(); // BAD: Magic number 200 // BAD: No timeout (can hang indefinitely) return c.json({ status: "ok" }, 200); }); ``` **Why bad:** Checking DB on every LB ping = slow + DB load, no timeout = LB request can hang forever, 200 on failure = LB keeps routing to broken instance --- -
middleware.md 13.6 KB
# Hono + OpenAPI - Middleware Examples > Authentication, rate limiting, CORS, logging, and caching middleware patterns. See [core.md](core.md) for route setup patterns. **Prerequisites**: Understand route handler patterns from core examples first. --- ## Authentication Middleware ### Good Example - JWT with Type-Safe Variables ```typescript import { verify } from "hono/jwt"; import { createMiddleware } from "hono/factory"; const BEARER_PREFIX = "Bearer "; const BEARER_PREFIX_LENGTH = 7; const HTTP_STATUS_UNAUTHORIZED = 401; const JWT_ALGORITHM = "HS256"; // REQUIRED: Explicit algorithm to prevent algorithm confusion attacks type AuthVariables = { userId: string; userRole: "admin" | "user"; }; export const authMiddleware = createMiddleware<{ Variables: AuthVariables }>( async (c, next) => { const authHeader = c.req.header("Authorization"); if (!authHeader?.startsWith(BEARER_PREFIX)) { return c.json( { error: "unauthorized", message: "Missing or invalid Authorization header", }, HTTP_STATUS_UNAUTHORIZED, ); } const token = authHeader.slice(BEARER_PREFIX_LENGTH); try { // IMPORTANT: Always specify the algorithm explicitly (required since v4.11.4) const payload = await verify( token, process.env.JWT_SECRET!, JWT_ALGORITHM, ); if (!payload.userId || typeof payload.userId !== "string") { throw new Error("Invalid token payload"); } c.set("userId", payload.userId); c.set("userRole", (payload.role as "admin" | "user") || "user"); await next(); } catch (error) { return c.json( { error: "unauthorized", message: "Invalid or expired token" }, HTTP_STATUS_UNAUTHORIZED, ); } }, ); ``` **Why good:** Type-safe Variables means c.get("userId") is typed, explicit algorithm prevents algorithm confusion attacks (CVE-2026-22817), payload validation prevents accepting garbage tokens, default role prevents undefined access ### Good Example - JWK/JWKS Authentication (Asymmetric Keys) For OIDC/OAuth providers using asymmetric keys: ```typescript import { jwk } from "hono/jwk"; // REQUIRED since v4.11.4: Explicit algorithm allowlist for asymmetric verification const ALLOWED_ALGORITHMS = ["RS256"] as const; export const jwkMiddleware = jwk({ jwks_uri: "https://auth.example.com/.well-known/jwks.json", alg: ALLOWED_ALGORITHMS, // REQUIRED: prevents algorithm confusion attacks }); // Usage app.use("/api/*", jwkMiddleware); ``` **Why good:** Explicit algorithm allowlist prevents attackers from forcing symmetric verification with known keys, JWKS auto-fetches and caches public keys **Usage in route:** ```typescript const protectedRoute = createRoute({ method: "get", path: "/me", middleware: [authMiddleware] as const, // as const for type inference operationId: "getCurrentUser", tags: ["Auth"], responses: { 200: { description: "Current user", content: { "application/json": { schema: UserSchema } }, }, 401: { description: "Unauthorized", content: { "application/json": { schema: ErrorResponseSchema } }, }, }, }); app.openapi(protectedRoute, async (c) => { // Type-safe access to userId from middleware const userId = c.get("userId"); const userRole = c.get("userRole"); // Use your database solution to find user by ID const user = await findUserById(userId); return c.json(user, 200); }); ``` **Why good:** `as const` enables TypeScript to infer middleware types, 401 in responses shows auth requirement in docs ### Bad Example - Weak auth middleware ```typescript // BAD Example - Weak auth middleware const authMiddleware = async (c, next) => { const token = c.req.header("Authorization")?.replace("Bearer ", ""); // BAD: Magic string if (!token) { return c.json({ error: "Unauthorized" }, 401); // BAD: Magic number } // BAD: No algorithm specified - vulnerable to algorithm confusion attacks! // BAD: No payload validation const payload = await verify(token, process.env.JWT_SECRET!); // BAD: No type safety c.userId = payload.userId; await next(); }; ``` **Why bad:** Missing algorithm allows attackers to forge tokens via algorithm confusion (CVE-2026-22817), c.userId not typed = any access, no payload validation = trusts malicious tokens, "Unauthorized" gives attackers no info but also no help for debugging --- ## Rate Limiting Middleware ### Good Example - Rate Limiting with Headers ```typescript import { createMiddleware } from "hono/factory"; const HTTP_STATUS_TOO_MANY_REQUESTS = 429; const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute const MAX_REQUESTS_PER_WINDOW = 100; const RETRY_AFTER_SECONDS = 60; // Simple in-memory rate limiter (use a shared store for multi-instance deployments) const rateLimitStore = new Map<string, { count: number; resetTime: number }>(); export const rateLimitMiddleware = createMiddleware(async (c, next) => { const clientId = c.req.header("X-API-Key") || c.req.header("X-Forwarded-For") || "anonymous"; const now = Date.now(); let record = rateLimitStore.get(clientId); // Reset if window expired if (!record || now > record.resetTime) { record = { count: 0, resetTime: now + RATE_LIMIT_WINDOW_MS, }; } record.count++; rateLimitStore.set(clientId, record); const remaining = Math.max(0, MAX_REQUESTS_PER_WINDOW - record.count); const resetTime = Math.ceil((record.resetTime - now) / 1000); // Set rate limit headers (industry standard) c.header("X-RateLimit-Limit", String(MAX_REQUESTS_PER_WINDOW)); c.header("X-RateLimit-Remaining", String(remaining)); c.header("X-RateLimit-Reset", String(resetTime)); if (record.count > MAX_REQUESTS_PER_WINDOW) { c.header("Retry-After", String(RETRY_AFTER_SECONDS)); return c.json( { error: "rate_limit_exceeded", message: `Too many requests. Limit: ${MAX_REQUESTS_PER_WINDOW} per minute`, statusCode: HTTP_STATUS_TOO_MANY_REQUESTS, retryAfter: RETRY_AFTER_SECONDS, }, HTTP_STATUS_TOO_MANY_REQUESTS, ); } await next(); }); ``` **Why good:** X-RateLimit-\* headers let clients track usage before hitting limit, Retry-After enables proper backoff, API key fallback to IP covers both auth'd and anon **Usage:** ```typescript // Apply globally const app = new OpenAPIHono(); app.use("*", rateLimitMiddleware); // Or per-route const getJobsRoute = createRoute({ method: "get", path: "/jobs", middleware: [rateLimitMiddleware] as const, // ... rest of route config }); ``` ### Bad Example - No rate limiting headers ```typescript // BAD Example - No rate limiting headers or proper response const rateLimiter = async (c, next) => { const count = getRequestCount(c); if (count > 100) { // BAD: Magic number return c.json({ error: "Too many requests" }, 429); // BAD: Magic number, no headers } await next(); }; ``` **Why bad:** No headers = client can't implement proactive backoff, generic message doesn't tell them when to retry, magic 100 can't be tuned per deployment --- ## CORS Middleware ### Good Example - Secure CORS ```typescript import { cors } from "hono/cors"; const ALLOWED_ORIGINS = [ "https://app.example.com", "https://admin.example.com", process.env.NODE_ENV === "development" ? "http://localhost:3000" : "", ].filter(Boolean); const MAX_AGE_SECONDS = 86400; // 24 hours export const corsMiddleware = cors({ origin: (origin) => { // Allow requests with no origin (mobile apps, Postman) if (!origin) return "*"; // Check against allowlist if (ALLOWED_ORIGINS.includes(origin)) { return origin; } // Reject unknown origins return ""; }, allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allowHeaders: ["Content-Type", "Authorization", "X-API-Key"], exposeHeaders: [ "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset", ], credentials: true, maxAge: MAX_AGE_SECONDS, }); // Apply globally const app = new OpenAPIHono(); app.use("*", corsMiddleware); ``` **Why good:** Origin allowlist prevents CSRF from random sites, exposeHeaders lets client read rate limit headers, no-origin fallback handles mobile apps/Postman ### Bad Example - Insecure CORS ```typescript // BAD Example - Insecure CORS configuration import { cors } from "hono/cors"; app.use( "*", cors({ origin: "*", // BAD: Wildcard with credentials is forbidden credentials: true, // BAD: Can't use with wildcard maxAge: 86400, // BAD: Magic number }), ); ``` **Why bad:** `*` + credentials is rejected by browsers (spec violation), magic maxAge can't be tuned, missing exposeHeaders = client can't read rate limit --- ## Logging Middleware ### Good Example - Structured JSON Logging with PII Sanitization ```typescript import { randomUUID } from "crypto"; import { createMiddleware } from "hono/factory"; const LOG_LEVEL_INFO = "info"; const LOG_LEVEL_WARN = "warn"; const LOG_LEVEL_ERROR = "error"; const SLOW_REQUEST_THRESHOLD_MS = 1000; // PII patterns to sanitize const PII_PATTERNS = [ { regex: /\b[\w.-]+@[\w.-]+\.\w+\b/g, replacement: "[EMAIL]" }, { regex: /\b\d{3}-\d{2}-\d{4}\b/g, replacement: "[SSN]" }, { regex: /\b\d{16}\b/g, replacement: "[CARD]" }, ]; const sanitizePII = (data: any): any => { if (typeof data === "string") { let sanitized = data; for (const pattern of PII_PATTERNS) { sanitized = sanitized.replace(pattern.regex, pattern.replacement); } return sanitized; } if (Array.isArray(data)) { return data.map(sanitizePII); } if (typeof data === "object" && data !== null) { const sanitized: any = {}; for (const [key, value] of Object.entries(data)) { if (["password", "token", "apiKey", "secret"].includes(key)) { sanitized[key] = "[REDACTED]"; } else { sanitized[key] = sanitizePII(value); } } return sanitized; } return data; }; export const loggingMiddleware = createMiddleware(async (c, next) => { const correlationId = c.req.header("X-Correlation-ID") || randomUUID(); const startTime = Date.now(); c.set("correlationId", correlationId); console.log( JSON.stringify({ level: LOG_LEVEL_INFO, type: "request", correlationId, method: c.req.method, path: c.req.path, query: sanitizePII(c.req.query()), userAgent: c.req.header("User-Agent"), ip: c.req.header("X-Forwarded-For") || c.req.header("X-Real-IP"), timestamp: new Date().toISOString(), }), ); await next(); const duration = Date.now() - startTime; const status = c.res.status; console.log( JSON.stringify({ level: status >= 500 ? LOG_LEVEL_ERROR : status >= 400 ? LOG_LEVEL_WARN : LOG_LEVEL_INFO, type: "response", correlationId, method: c.req.method, path: c.req.path, status, duration, slow: duration > SLOW_REQUEST_THRESHOLD_MS, timestamp: new Date().toISOString(), }), ); c.header("X-Correlation-ID", correlationId); }); app.use("*", loggingMiddleware); ``` **Why good:** Correlation IDs trace requests across services, PII sanitization = GDPR compliance, structured JSON = searchable in log aggregators, duration tracking finds slow endpoints ### Bad Example - No structure or sanitization ```typescript // BAD Example - No structure or sanitization app.use("*", async (c, next) => { // BAD: Logs PII without sanitization // BAD: No correlation ID // BAD: No structured format console.log(`${c.req.method} ${c.req.path}`, c.req.query()); await next(); // BAD: No duration tracking console.log(`Response: ${c.res.status}`); }); ``` **Why bad:** Logging PII = GDPR violation, no correlation = can't trace user's request across microservices, unstructured = grep only (not searchable in log aggregators) --- ## Caching Middleware ### Good Example - Cache-Control Headers ```typescript import { createMiddleware } from "hono/factory"; const CACHE_MAX_AGE_SECONDS = 3600; const CACHE_STALE_WHILE_REVALIDATE_SECONDS = 86400; export const cacheMiddleware = createMiddleware(async (c, next) => { await next(); // Only cache successful GET requests if (c.req.method === "GET" && c.res.status === 200) { // Public resources (job listings, company profiles) if ( c.req.path.startsWith("/api/jobs") || c.req.path.startsWith("/api/companies") ) { c.header( "Cache-Control", `public, max-age=${CACHE_MAX_AGE_SECONDS}, stale-while-revalidate=${CACHE_STALE_WHILE_REVALIDATE_SECONDS}`, ); } // Private resources (user data) if (c.req.path.startsWith("/api/me")) { c.header("Cache-Control", "private, max-age=0, must-revalidate"); } } // Never cache errors if (c.res.status >= 400) { c.header("Cache-Control", "no-store"); } }); ``` **Why good:** stale-while-revalidate serves cached while fetching new (fast UX), public/private prevents caching user data, no caching errors prevents stale failures ### Good Example - ETags ```typescript import { createHash } from "crypto"; const HTTP_STATUS_NOT_MODIFIED = 304; app.openapi(getJobsRoute, async (c) => { const jobs = await fetchJobs(); const jobsJson = JSON.stringify(jobs); // Generate ETag from response content const etag = createHash("md5").update(jobsJson).digest("hex"); // Check If-None-Match header const clientEtag = c.req.header("If-None-Match"); if (clientEtag === etag) { return c.body(null, HTTP_STATUS_NOT_MODIFIED); } c.header("ETag", etag); return c.json(jobs, 200); }); ``` **Why good:** 304 sends no body = massive bandwidth savings, ETag comparison is cheap (hash compare), client caches intelligently --- -
openapi.md 2 KB
# Hono + OpenAPI - Spec Generation Examples > OpenAPI specification generation patterns. See [core.md](core.md) for route setup patterns. **Prerequisites**: Understand route definition patterns from core examples first. --- ## Pattern 1: Build-Time Spec Generation ### Good Example - Build-Time Spec Generation **File: `/scripts/generate-openapi.ts`** ```typescript import { writeFileSync } from "fs"; import { app } from "../app/api/[[...route]]/route"; const API_VERSION = "1.0.0"; const INDENT_SPACES = 2; // getOpenAPI31Document requires a config object with openapi version and info const spec = app.getOpenAPI31Document({ openapi: "3.1.0", info: { title: "Jobs API", version: API_VERSION, description: "API for managing job postings", }, servers: [ { url: "http://localhost:3000/api", description: "Local development" }, { url: "https://api.example.com/api", description: "Production" }, ], }); const outputPath = "./public/openapi.json"; writeFileSync(outputPath, JSON.stringify(spec, null, INDENT_SPACES)); console.log(`OpenAPI spec written to ${outputPath}`); ``` **Why good:** Build-time = spec generated once (fast), env-specific servers = proper URLs in docs, config object provides required OpenAPI metadata You can also use `app.doc31("/doc", config)` to serve the spec at a route endpoint, but build-time generation is preferred for client code generators. **Package.json:** ```json { "scripts": { "prebuild": "bun run scripts/generate-openapi.ts && openapi-ts" } } ``` ### Bad Example - Runtime spec generation ```typescript // BAD Example - Runtime spec generation app.get("/openapi.json", (c) => { // BAD: Generates spec on every request (slow) // BAD: No servers config for client generators return c.json( app.getOpenAPI31Document({ openapi: "3.1.0", info: { title: "API", version: "1.0.0" }, }), ); }); ``` **Why bad:** Runtime = regenerates on every request (CPU cost), no servers config = no proper URLs for clients, can't use client generators at build time --- -
routes.md 5.9 KB
# Hono + OpenAPI - Route Examples > Filtering, pagination, and data transformation patterns. See [core.md](core.md) for basic route setup. **Prerequisites**: Understand Pattern 2 (List Endpoint) and Pattern 3 (Detail Endpoint) from core examples first. --- ## Filtering Patterns ### Good Example - Comma-Separated Multi-Value Filters The Hono route validates and extracts query params. The filter logic shows the pattern for parsing comma-separated values. ```typescript app.openapi(getJobsRoute, async (c) => { const { country, employment_type } = c.req.valid("query"); // Parse comma-separated values and normalize case const filters: Record<string, string[]> = {}; if (country) { filters.country = country.split(",").map((c) => c.trim().toLowerCase()); } if (employment_type) { filters.employment_type = employment_type.split(",").map((e) => e.trim()); } // Pass normalized filters to your database query layer // Use case-insensitive matching (LOWER() or ILIKE) for text fields const results = await fetchJobs({ filters, limit: DEFAULT_QUERY_LIMIT }); return c.json({ jobs: results, total: results.length }, 200); }); ``` **Why good:** comma-separated values support `?country=germany,france,spain` in one request, case normalization prevents "Germany" vs "germany" mismatches, `c.req.valid()` ensures type-safe extraction ### Bad Example - No multiple value support ```typescript // BAD Example app.get("/jobs", async (c) => { // BAD: c.req.query() bypasses Zod validation const country = c.req.query("country"); // BAD: Only handles single value, case-sensitive const results = await fetchJobs({ country, limit: 100 }); // BAD: Magic number return c.json({ jobs: results }); }); ``` **Why bad:** `c.req.query()` bypasses validation, single-value filter forces multiple API calls, case-sensitive breaks user input, magic 100 --- ## Pagination Patterns ### Good Example - Offset-Based Pagination ```typescript const DEFAULT_LIMIT = 50; const DEFAULT_OFFSET = 0; const RADIX_DECIMAL = 10; export const parsePagination = (limit?: string, offset?: string) => ({ limit: limit ? parseInt(limit, RADIX_DECIMAL) : DEFAULT_LIMIT, offset: offset ? parseInt(offset, RADIX_DECIMAL) : DEFAULT_OFFSET, }); app.openapi(getJobsRoute, async (c) => { const query = c.req.valid("query"); const { limit, offset } = parsePagination(query.limit, query.offset); // Use your database solution with limit/offset const results = await fetchJobs({ limit, offset }); // Get total count with same filters for pagination UI const total = await countJobs(); return c.json({ jobs: results, total, limit, offset }, 200); }); ``` **Why good:** Total count enables "Page X of Y" UI, radix 10 prevents `parseInt("08")` bugs, returning limit/offset in response lets clients track state **Pagination response schema:** ```typescript export const PaginatedJobsResponseSchema = z .object({ jobs: z.array(JobSchema), total: z.number().int().min(0), limit: z.number().int().min(1), offset: z.number().int().min(0), }) .openapi("PaginatedJobsResponse"); ``` ### Bad Example - Missing best practices ```typescript // BAD Example - Missing best practices const limit = parseInt(c.req.query("limit") || "50"); // BAD: Magic numbers, no radix const offset = parseInt(c.req.query("offset") || "0"); // BAD: Magic numbers, no radix const results = await fetchJobs({ limit, offset }); // BAD: No total count - can't show "Page X of Y" return c.json({ jobs: results }); ``` **Why bad:** No radix causes `parseInt("08")=0` in some engines, missing total = can't build pagination UI, limit/offset not in response = client can't track state --- ## Data Transformation Patterns ### Good Example - Reusable Transformation Utilities **File: `/app/api/utils/helpers.ts`** ```typescript export const toISOString = (date: Date | string | null): string | null => { if (!date) return null; return date instanceof Date ? date.toISOString() : date; }; const DEFAULT_CURRENCY = "EUR"; export const transformJobRow = (row: any) => { return { id: row.id, title: row.title, description: row.description, employmentType: row.employmentType, // Conditionally include salary only if showSalary is true salary: row.showSalary && row.salaryMin && row.salaryMax ? { min: row.salaryMin, max: row.salaryMax, currency: row.salaryCurrency || DEFAULT_CURRENCY, } : null, // Transform dates to ISO strings postedDate: toISOString(row.postedDate), createdAt: toISOString(row.createdAt)!, // Flatten joined company data into nested object company: { name: row.companyName, logoUrl: row.companyLogoUrl, }, }; }; ``` **Why good:** Reusable transform = DRY across routes, null-safe toISOString prevents crashes, conditional salary inclusion respects showSalary flag **Usage in route:** ```typescript app.openapi(getJobsRoute, async (c) => { // Fetch raw rows from your database const rows = await fetchJobRows(); // Transform all rows using the reusable utility const transformedJobs = rows.map(transformJobRow); return c.json({ jobs: transformedJobs, total: transformedJobs.length }, 200); }); ``` ### Bad Example - Inline transformations ```typescript // BAD Example - Inline transformations app.get("/jobs", async (c) => { const rows = await fetchJobRows(); // BAD: Inline transformation makes code hard to read // BAD: No reusability across routes // BAD: Magic string "EUR" const jobs = rows.map((r) => ({ ...r, salary: r.showSalary ? { min: r.salaryMin, max: r.salaryMax, currency: r.salaryCurrency || "EUR", // BAD: Magic string } : null, createdAt: r.createdAt.toISOString(), // BAD: Can crash if null })); return c.json(jobs); }); ``` **Why bad:** Inline transform duplicates across routes, r.createdAt.toISOString() crashes on null, magic "EUR" becomes inconsistent when changed --- -
validation.md 3.7 KB
# Hono + OpenAPI - Validation Examples > Zod schema definitions with OpenAPI integration. See [core.md](core.md) for route setup patterns. **Prerequisites**: Understand Pattern 1 (Modular Route Setup) from core examples first. --- ## Pattern 1: Complete Schema Setup ### Good Example - Complete Schema Setup **File: `/app/api/schemas.ts`** ```typescript // Import z from @hono/zod-openapi — NOT from "zod" // This re-export already has .openapi() available on all Zod types import { z } from "@hono/zod-openapi"; const MIN_SALARY = 0; const CURRENCY_CODE_LENGTH = 3; const MIN_TITLE_LENGTH = 1; const MAX_TITLE_LENGTH = 255; const DEFAULT_LIMIT = "50"; // Reusable Sub-Schemas 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 CompanySchema = z .object({ name: z.string().nullable(), logoUrl: z.string().url().nullable(), }) .openapi("Company"); // Request Schemas export const JobsQuerySchema = z .object({ country: z .string() .optional() .openapi({ param: { name: "country", in: "query" }, example: "germany", description: "Filter by country (comma-separated for multiple)", }), employment_type: z .enum(["full_time", "part_time", "contract", "internship"]) .optional() .openapi({ param: { name: "employment_type", in: "query" }, example: "full_time", }), limit: z .string() .regex(/^\d+$/) .optional() .openapi({ param: { name: "limit", in: "query" }, example: DEFAULT_LIMIT, }), }) .openapi("JobsQuery"); // Response Schemas 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(), company: CompanySchema, }) .openapi("Job"); export const JobsResponseSchema = z .object({ jobs: z.array(JobSchema), total: z.number().int().min(MIN_SALARY), }) .openapi("JobsResponse"); export const ErrorResponseSchema = z .object({ error: z.string(), message: z.string(), }) .openapi("ErrorResponse", { example: { error: "Failed to fetch jobs", message: "Database connection timeout", }, }); // Type Exports export type Salary = z.infer<typeof SalarySchema>; export type Company = z.infer<typeof CompanySchema>; export type Job = z.infer<typeof JobSchema>; export type JobsQuery = z.infer<typeof JobsQuerySchema>; export type JobsResponse = z.infer<typeof JobsResponseSchema>; ``` **Why good:** `z` from `@hono/zod-openapi` provides `.openapi()` automatically, named constants prevent magic number bugs, reusable sub-schemas reduce duplication, `.openapi("Name")` registers as `#/components/schemas/Name` ### Bad Example - Missing best practices ```typescript // BAD Example - Missing best practices import { z } from "zod"; const JobSchema = z.object({ id: z.string(), title: z.string().min(1).max(255), // BAD: Magic numbers salary: z.object({ // BAD: Duplicated schema instead of reusable min: z.number(), max: z.number(), currency: z.string().length(3), // BAD: Magic number }), }); // BAD: No .openapi() registration // BAD: No examples for documentation // BAD: z imported from "zod" instead of "@hono/zod-openapi" export default JobSchema; // BAD: Default export ``` **Why bad:** Magic numbers cause silent bugs when changed, importing `z` from `"zod"` means `.openapi()` is not available, no `.openapi()` = no docs, duplicated schemas diverge over time ---
-
-
reference.md 12.7 KB
# Backend API Reference > Decision frameworks, anti-patterns, and red flags for Hono + OpenAPI. Referenced from [SKILL.md](SKILL.md). --- <decision_framework> ## Decision Framework See [SKILL.md](SKILL.md) for when to use / when NOT to use Hono + OpenAPI. ### Pagination Decision **Offset-based:** Most CRUD (simple). Use for standard list views. **Cursor-based:** Real-time feeds or >100k rows (no page skipping, but handles inserts during pagination). ### Rate Limiting Decision **When to use:** Any public API, APIs with external consumers, production deployments. **When not to use:** Internal APIs behind VPN/firewall don't need rate limiting overhead. ### CORS Decision **When to use:** APIs consumed by web apps from different origins. **When not to use:** Same-origin only APIs (no external consumers) don't need CORS config. ### Health Check Decision **Shallow `/health`:** Fast, no dependency checks. Use for load balancer liveness probes. **Deep `/health/deep`:** Includes dependency checks. Use for readiness probes and monitoring. ### RPC vs REST Client Decision (v4.x) **Use Hono RPC (`hc`):** - Full-stack TypeScript monorepo - Same Hono version on client and server - Want end-to-end type safety without code generation - Building internal APIs consumed by your own frontend **Use REST/OpenAPI Client:** - Multi-language clients (Python, Go, etc.) - External consumers need generated SDKs - Different teams own server and client - Need formal OpenAPI documentation ### Context Storage vs Props Drilling Decision (v4.6.0+) **Use Context Storage (`getContext`):** - Accessing Cloudflare Workers bindings in utility functions - Deep call stacks where passing context is cumbersome - Typed variables needed across module boundaries **Use Props Drilling:** - Simple handlers with shallow call stacks - Testing is easier with explicit dependencies - Functions need to work outside request context ### Combine Middleware Decision **Use `some()`:** First successful auth wins (premium API key skips rate limiting) **Use `every()`:** All checks must pass (IP restriction AND bearer auth) **Use `except()`:** Apply middleware to all paths except specific ones (public endpoints) </decision_framework> --- <red_flags> ## RED FLAGS ### High Priority Issues - **Importing `z` from `"zod"` instead of `"@hono/zod-openapi"`** -- `.openapi()` won't be available on schemas - **Not using `.openapi()` on Zod schemas** -- OpenAPI spec won't include schema metadata or examples - **Not handling validation errors properly** -- Hono validates, but you must return proper error shapes - **Not exporting `app` instance** -- can't generate OpenAPI spec at build time - **Missing `operationId` in routes** -- generated client has ugly method names like `get_api_v1_jobs` - **JWT/JWK without explicit `alg` option** -- vulnerable to algorithm confusion attacks (CVE-2026-22817, CVE-2026-22818) allowing forged tokens ### Medium Priority Issues - **Queries without soft delete checks** - Returns deleted records to users - **No pagination limits** - Can return massive datasets and crash clients - **Generating spec at runtime** - Use build-time generation (prebuild script) - **Missing error logging** - Can't debug production issues - **No total count in paginated responses** - Can't build proper pagination UI - **No rate limiting on public APIs** - Vulnerable to abuse and DDoS - **Wildcard CORS with credentials** - Security violation (browsers reject this) - **Missing health check endpoints** - Can't monitor or auto-scale properly - **Logging PII without sanitization** - GDPR/compliance violations ### Common Mistakes - **Not using `c.req.valid()` for params** - Bypasses validation entirely - **Using `parseInt()` without radix** - Can cause bugs (always use `parseInt(str, 10)`) - **Transforming data in queries** - Do it in transformation utilities for reusability - **Inline route handlers** - Create God files (use `app.route()` for modularization) - **Case-sensitive filters** - Poor UX (use `LOWER()` for text comparisons) - **Not returning proper status codes** - Always specify (200, 404, 500, etc.) - **Missing context in console.error** - Log operation name with error - **No rate limit headers** - Clients can't track usage or implement backoff - **Health checks without timeouts** - Can hang load balancers indefinitely - **No correlation IDs in logs** - Can't trace requests across services - **Missing Cache-Control headers** - Unnecessary server load and slow responses ### Gotchas & Edge Cases - **Health checks with orchestrators:** Use `/health` for liveness probes, `/health/deep` for readiness probes - **Rate limiting in multi-instance:** In-memory stores don't work across instances - use a shared store - **CORS preflight:** OPTIONS requests bypass auth middleware - configure CORS before auth - **ETags with dynamic content:** Don't use for user-specific data (generates new ETag per user) - **Correlation IDs:** Forward from client if present (`X-Correlation-ID` header) - **JWT `alg` option:** Required since v4.11.4 -- omitting it allows algorithm confusion attacks (CVE-2026-22817, CVE-2026-22818) where attackers forge tokens - **RPC version mismatch:** Client and server MUST use same Hono version for RPC types to work correctly - **RPC route chaining:** Routes must be chained (`.get().post()`) for type inference - separate `app.get()` calls break inference - **RPC TypeScript strict mode:** Both client and server `tsconfig.json` MUST have `"strict": true` for RPC type inference to work properly - **Context Storage requires middleware:** `contextStorage()` middleware must be registered BEFORE any code calls `getContext()` - **getContext() throws:** Use `tryGetContext()` (v4.11.0+) in code that may run outside request context (tests, background jobs) - **Middleware `next()` never throws:** Since Hono catches errors, wrapping `await next()` in try/catch is unnecessary - **some/every execution order:** Middleware in `some()` runs sequentially until first success; in `every()` runs until first failure - **getConnInfo is adapter-specific:** Import from `hono/bun`, `hono/deno`, `@hono/node-server/conninfo`, or `hono/cloudflare-workers` -- NOT from `hono/ip-restriction` </red_flags> --- <anti_patterns> ## Anti-Patterns to Avoid ### Inline Route Handlers Without Modularization ```typescript // ANTI-PATTERN: All routes in one file const app = new OpenAPIHono(); app.get("/jobs", async (c) => { /* 100+ lines */ }); app.get("/jobs/:id", async (c) => { /* 100+ lines */ }); app.get("/companies", async (c) => { /* 100+ lines */ }); app.get("/users", async (c) => { /* 100+ lines */ }); // ... 1000+ line file ``` **Why it's wrong:** Creates God files that are unmaintainable, no separation of concerns, hard to test individual routes. **What to do instead:** Use `app.route()` to mount modular route files. --- ### Missing OpenAPI Schema Registration ```typescript // ANTI-PATTERN: Zod schema without .openapi() const JobSchema = z.object({ id: z.string(), title: z.string().min(1).max(255), }); app.get("/jobs", async (c) => { return c.json({ jobs: [] }); }); ``` **Why it's wrong:** No OpenAPI spec generation, no auto-documentation, loses type safety benefits. **What to do instead:** Import `z` from `@hono/zod-openapi` (not `zod`), then use `.openapi()` on schemas. --- ### Magic Numbers in API Code ```typescript // ANTI-PATTERN: Magic numbers everywhere const results = await fetchJobs({ limit: 100 }); // What does 100 mean? if (count > 50) { // Why 50? return c.json({ error: "Rate limited" }, 429); } c.header("Cache-Control", "max-age=3600"); // What does 3600 represent? ``` **Why it's wrong:** Numbers scattered across code, impossible to tune, no documentation of intent. **What to do instead:** Use named constants like `DEFAULT_QUERY_LIMIT = 100`, `CACHE_MAX_AGE_SECONDS = 3600`. --- ### Validation Bypass ```typescript // ANTI-PATTERN: Reading params directly without validation app.get("/jobs/:id", async (c) => { const id = c.req.param("id"); // No validation! const country = c.req.query("country"); // Could be undefined // Unvalidated input goes straight to database const job = await findJobById(id); }); ``` **Why it's wrong:** Bypasses Zod validation, no type safety, crashes on bad input. **What to do instead:** Always use `c.req.valid("param")` and `c.req.valid("query")` with createRoute. --- ### Insecure CORS Configuration ```typescript // ANTI-PATTERN: Wildcard + credentials app.use( "*", cors({ origin: "*", credentials: true, // Browsers will reject this! }), ); ``` **Why it's wrong:** Violates CORS spec, browsers reject wildcard with credentials. **What to do instead:** Use origin allowlist with explicit origins. --- ### Slow Health Checks ```typescript // ANTI-PATTERN: Heavy checks on every ping app.get("/health", async (c) => { await checkDatabase(); await checkRedis(); await checkExternalService(); return c.json({ status: "ok" }); }); ``` **Why it's wrong:** Load balancers ping frequently, this creates unnecessary load. **What to do instead:** Use shallow `/health` for liveness, deep `/health/deep` for readiness. --- ### Logging PII Without Sanitization ```typescript // ANTI-PATTERN: Logging user data directly app.use("*", async (c, next) => { console.log("Request body:", await c.req.json()); await next(); }); ``` **Why it's wrong:** Logs may contain emails, passwords, credit cards - GDPR violation. **What to do instead:** Sanitize PII patterns and redact sensitive fields before logging. --- ### No Error Context ```typescript // ANTI-PATTERN: Generic error handling try { // ... operation } catch (error) { return c.json({ error: "Error" }, 500); } ``` **Why it's wrong:** Can't debug in production, no correlation, no operation context. **What to do instead:** Log with correlation ID, operation name, and structured format. --- ### Missing RPC Route Chaining ```typescript // ANTI-PATTERN: Separate route definitions break type inference const app = new OpenAPIHono(); app.openapi(getUserRoute, async (c) => { /* ... */ }); app.openapi(createUserRoute, async (c) => { /* ... */ }); export type AppType = typeof app; // Types won't include route details! ``` **Why it's wrong:** Separate `app.openapi()` calls don't chain types - client won't have route type info. **What to do instead:** Chain route definitions: `const routes = app.openapi(route1, handler1).openapi(route2, handler2);` then export `typeof routes`. --- ### Using getContext() Without contextStorage Middleware ```typescript // ANTI-PATTERN: getContext() without middleware setup import { getContext } from "hono/context-storage"; // This will throw "Context is not available" error! export function getDatabase() { const ctx = getContext(); return ctx.env.DATABASE; } ``` **Why it's wrong:** `getContext()` requires `contextStorage()` middleware to be registered first. **What to do instead:** Register `app.use(contextStorage())` before any routes, or use `tryGetContext()` for optional access. --- ### Complex Auth Logic Without Combine Middleware ```typescript // ANTI-PATTERN: Nested if/else auth logic app.use("/api/*", async (c, next) => { const token = c.req.header("Authorization"); const ip = c.req.header("X-Forwarded-For"); if (token === ADMIN_TOKEN) { await next(); } else if (INTERNAL_IPS.includes(ip || "")) { if (token === INTERNAL_TOKEN) { await next(); } else { return c.json({ error: "Unauthorized" }, 401); } } else { return c.json({ error: "Unauthorized" }, 401); } }); ``` **Why it's wrong:** Hard to read, hard to test, easy to have logic bugs. **What to do instead:** Use `some()`, `every()`, `except()` from `hono/combine` for declarative auth composition. </anti_patterns> --- ## Production Checklist ### Before Deploying API Routes - [ ] `z` imported from `@hono/zod-openapi` (not `zod`) - [ ] All schemas have `.openapi()` registration - [ ] All routes have `operationId` - [ ] App instance exported for spec generation - [ ] Error handling uses named constants - [ ] No magic numbers - [ ] Soft delete checks on all queries - [ ] Pagination with total count - [ ] Rate limiting configured (if public) - [ ] CORS configured (if cross-origin) - [ ] Health check endpoints implemented - [ ] Logging with correlation IDs - [ ] PII sanitization in logs - [ ] Cache-Control headers set - [ ] JWT/JWK middleware has explicit `alg` option (security requirement since v4.11.4) ### Hono v4.x Features Checklist - [ ] If using RPC: Routes chained for type inference, `AppType` exported - [ ] If using RPC: Same Hono version on client and server - [ ] If using Context Storage: `contextStorage()` middleware registered first - [ ] If accessing context in utilities: Use `tryGetContext()` for optional access - [ ] If complex auth logic: Use `some`/`every`/`except` from `hono/combine` -
SKILL.md 9.3 KB
--- 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. Import `z` from `@hono/zod-openapi` (NOT from `zod`) so `.openapi()` is available on all schemas. Always include `operationId` in routes and export the `app` instance for spec 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 import `z` from `@hono/zod-openapi`, NOT from `zod` -- this gives Zod the `.openapi()` method)** **(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 with auto-generated OpenAPI specs - Defining OpenAPI specifications with automatic Zod validation - Creating standardized error responses with proper status codes - Implementing filtering, pagination, and sorting patterns - Public or multi-client APIs needing formal documentation - Production APIs requiring rate limiting, CORS, health checks **When NOT to use:** - Simple CRUD with no external consumers (framework-native endpoints are simpler) - Internal-only APIs without documentation requirements - Single-use endpoints with no schema reuse (over-engineering) **Key patterns covered:** - Modular route setup with `app.route()` and `OpenAPIHono` - Zod schema definitions with `.openapi()` metadata - Route definition with `createRoute` (operationId, tags, responses) - Error handling with named error codes - Filtering, pagination, and data transformation - Auth, rate limiting, CORS, logging, caching middleware - Health check endpoints (shallow and deep) - RPC client (`hc`) with end-to-end type safety - Context Storage for out-of-handler context access - Combine Middleware (`some`/`every`/`except`) for declarative auth **Detailed Resources:** - [examples/core.md](examples/core.md) - Route setup, list/detail endpoints - [examples/validation.md](examples/validation.md) - Zod schema definitions with OpenAPI - [examples/routes.md](examples/routes.md) - Filtering, pagination, data transformation - [examples/middleware.md](examples/middleware.md) - Auth, rate limiting, CORS, logging, caching - [examples/error-handling.md](examples/error-handling.md) - Standardized error responses - [examples/openapi.md](examples/openapi.md) - Spec generation (build-time and endpoint) - [examples/health-checks.md](examples/health-checks.md) - Liveness and readiness checks - [examples/advanced-v4.md](examples/advanced-v4.md) - RPC, Context Storage, Combine Middleware - [reference.md](reference.md) - Decision frameworks, anti-patterns, production checklist --- <philosophy> ## Philosophy **Type safety + documentation from code.** Zod schemas serve both validation AND OpenAPI spec generation. Single source of truth flows to clients via generated SDKs or Hono's RPC client. **Use Hono + OpenAPI when:** Building public/multi-client APIs, need auto-generated documentation, require formal OpenAPI specs, want type-safe validation. **Use simpler approaches when:** Internal-only CRUD, no external API consumers, no documentation needs. </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Modular Route Setup Structure routes using `app.route()` for modularization. Export the `app` instance for spec generation. ```typescript import { OpenAPIHono } from "@hono/zod-openapi"; const app = new OpenAPIHono().basePath("/api"); app.route("/", jobsRoutes); app.route("/", companiesRoutes); // REQUIRED: Export app for spec generation export { app }; ``` **Why good:** `app.route()` prevents God files, app export enables build-time spec generation See [examples/core.md](examples/core.md) for complete setup with framework adapter exports. --- ### Pattern 2: Zod Schemas with OpenAPI Metadata Import `z` from `@hono/zod-openapi` (not `zod`). Use `.openapi()` for schema registration and documentation. ```typescript import { z } from "@hono/zod-openapi"; const MIN_SALARY = 0; const CURRENCY_CODE_LENGTH = 3; 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" }, }); ``` **Why good:** importing `z` from `@hono/zod-openapi` provides `.openapi()` automatically, named constants prevent magic number bugs, `.openapi("Name")` registers as `#/components/schemas/Name` See [examples/validation.md](examples/validation.md) for complete schema patterns. --- ### Pattern 3: Route Definition with createRoute Define routes with `createRoute` and implement with `app.openapi()`. Always include `operationId`. ```typescript import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi"; const getJobsRoute = createRoute({ method: "get", path: "/jobs", operationId: "getJobs", // Becomes client method name tags: ["Jobs"], request: { query: JobsQuerySchema }, responses: { 200: { description: "List of jobs", content: { "application/json": { schema: JobsResponseSchema } }, }, }, }); app.openapi(getJobsRoute, async (c) => { const { country } = c.req.valid("query"); // Type-safe validated params // ... handler logic return c.json({ jobs: results }, 200); }); ``` **Why good:** `operationId` becomes clean client method name (`getJobs` vs `get_api_jobs`), `c.req.valid()` enforces schema validation with types See [examples/core.md](examples/core.md) for list/detail endpoint examples. --- ### Pattern 4: Error Handling with Named Codes Use named error code constants for consistent, machine-parseable error responses. ```typescript export const ErrorCodes = { VALIDATION_ERROR: "validation_error", NOT_FOUND: "not_found", UNAUTHORIZED: "unauthorized", INTERNAL_ERROR: "internal_error", } as const; ``` **Why good:** Named codes enable frontend `switch` handling, consistent shape across all endpoints See [examples/error-handling.md](examples/error-handling.md) for the full `handleRouteError` utility. --- ### Pattern 5: JWT Authentication with Explicit Algorithm Always specify the `alg` option on JWT/JWK middleware to prevent algorithm confusion attacks (CVE-2026-22817, CVE-2026-22818, patched in v4.11.4+). ```typescript import { verify } from "hono/jwt"; const JWT_ALGORITHM = "HS256"; const payload = await verify(token, secret, JWT_ALGORITHM); ``` **Why good:** explicit algorithm prevents attackers from switching to symmetric verification with known public keys See [examples/middleware.md](examples/middleware.md) for complete auth middleware with type-safe variables. </patterns> --- <red_flags> ## RED FLAGS **High Priority:** - Importing `z` from `"zod"` instead of `"@hono/zod-openapi"` -- `.openapi()` won't be available - Missing `operationId` in routes -- generated client has ugly method names - Not exporting `app` instance -- can't generate OpenAPI spec at build time - JWT/JWK without explicit `alg` option -- algorithm confusion vulnerability (CVE-2026-22817/22818) **Medium Priority:** - Using `c.req.param()` / `c.req.query()` instead of `c.req.valid()` -- bypasses Zod validation - No pagination limits on list endpoints -- returns massive datasets - Generating spec at runtime instead of build time -- wasted CPU per request - Not returning proper status codes -- always specify (200, 404, 500) - Wildcard CORS (`"*"`) with `credentials: true` -- browsers reject this (spec violation) **Gotchas & Edge Cases:** - `c.req.valid("param")` uses singular `"param"`, not `"params"` -- easy to mistype - In-memory rate limiting doesn't work across multiple instances -- use a shared store - CORS middleware must be registered before auth middleware -- OPTIONS preflight bypasses auth - ETags should not be used for user-specific data (generates unique ETag per user) - RPC routes must be chained (`.openapi(r1, h1).openapi(r2, h2)`) for type inference -- separate calls break it - Both client and server `tsconfig.json` need `"strict": true` for RPC type inference - `contextStorage()` middleware must be registered before any code calls `getContext()` - Use `tryGetContext()` (v4.11.0+) in code that may run outside request context (tests, background jobs) - Middleware `next()` never throws in Hono -- wrapping `await next()` in try/catch is unnecessary - `getConnInfo` is adapter-specific -- import from `hono/bun`, `hono/deno`, `@hono/node-server/conninfo`, etc. (NOT from `hono/ip-restriction`) </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST import `z` from `@hono/zod-openapi`, NOT from `zod` -- this gives Zod the `.openapi()` method)** **(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 and type safety.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.