Understand the project structure and how components work together
The Hono OpenAPI Starter follows a modular architecture that separates concerns and promotes maintainability. This guide explains how the different components work together to create a type-safe, well-documented API.
The application starts in app.ts, which orchestrates the setup:
src/app.ts
import configureOpenAPI from "@/lib/configure-open-api";import createApp from "@/lib/create-app";import index from "@/routes/index.route";import tasks from "@/routes/tasks/tasks.index";const app = createApp();configureOpenAPI(app);const routes = [ index, tasks,] as const;routes.forEach((route) => { app.route("/", route);});export type AppType = typeof routes[number];export default app;
The AppType export is crucial for end-to-end type safety. It enables type-safe API clients using Hono’s RPC feature.
The createApp() function establishes the core application with middleware:
src/lib/create-app.ts
import { OpenAPIHono } from "@hono/zod-openapi";import { requestId } from "hono/request-id";import { notFound, onError, serveEmojiFavicon } from "stoker/middlewares";import { defaultHook } from "stoker/openapi";export function createRouter() { return new OpenAPIHono<AppBindings>({ strict: false, defaultHook, });}export default function createApp() { const app = createRouter(); app.use(requestId()) .use(serveEmojiFavicon("📝")) .use(pinoLogger()); app.notFound(notFound); app.onError(onError); return app;}
What is the defaultHook?
The defaultHook from Stoker handles validation errors automatically. When a request fails Zod validation, it returns a properly formatted 422 Unprocessable Entity response with detailed error information.
Route definitions declare the API contract using createRoute():
src/routes/tasks/tasks.routes.ts
import { createRoute, z } from "@hono/zod-openapi";import { jsonContent, jsonContentRequired } from "stoker/openapi/helpers";import { insertTasksSchema, selectTasksSchema } from "@/db/schema";export const create = createRoute({ path: "/tasks", method: "post", request: { body: jsonContentRequired( insertTasksSchema, "The task to create", ), }, tags: ["Tasks"], responses: { [HttpStatusCodes.OK]: jsonContent( selectTasksSchema, "The created task", ), [HttpStatusCodes.UNPROCESSABLE_ENTITY]: jsonContent( createErrorSchema(insertTasksSchema), "The validation error(s)", ), },});
Each route definition specifies request validation, response schemas, and OpenAPI documentation in one place. This single source of truth ensures your docs are always in sync with your implementation.
import type { AppRouteHandler } from "@/lib/types";import type { CreateRoute } from "./tasks.routes";import db from "@/db";import { tasks } from "@/db/schema";export const create: AppRouteHandler<CreateRoute> = async (c) => { const task = c.req.valid("json"); const [inserted] = await db.insert(tasks).values(task).returning(); return c.json(inserted, HttpStatusCodes.OK);};
Notice how c.req.valid("json") returns a fully typed object. The AppRouteHandler<CreateRoute> type ensures the handler matches the route definition, providing complete type safety.
import { drizzle } from "drizzle-orm/libsql";import * as schema from "./schema";const db = drizzle({ connection: { url: env.DATABASE_URL, authToken: env.DATABASE_AUTH_TOKEN, }, casing: "snake_case", schema,});export default db;
The casing: "snake_case" option automatically converts JavaScript camelCase to database snake_case, keeping your code idiomatic while following SQL conventions.
The application defines global types for consistency:
src/lib/types.ts
import type { OpenAPIHono, RouteConfig, RouteHandler } from "@hono/zod-openapi";import type { PinoLogger } from "hono-pino";export interface AppBindings { Variables: { logger: PinoLogger; };};export type AppOpenAPI<S extends Schema = {}> = OpenAPIHono<AppBindings, S>;export type AppRouteHandler<R extends RouteConfig> = RouteHandler<R, AppBindings>;
What are AppBindings?
AppBindings define the context variables available in your route handlers. Here, it provides access to the logger instance. You can extend this to include database connections, authentication data, or any other request-scoped values.