Claude Skill

api-framework-hono

Hono routes, OpenAPI, Zod validation

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-framework-hono_skills_api-framework-hono-3a51ef5.zip · 27 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-framework-hono/skills/api-framework-hono
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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-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:




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

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.

No comments yet.

Reviews (0)

No reviews yet.

Related