API Hono Overview
Hono structure in create-light-stack.
Project Structure
Code is organised inside src for clear separation of concerns.
src/
├── controllers/ # Request handlers (business logic)
├── middlewares/ # Interceptors (Auth, Errors)
├── routes/ # Route definitions and method mapping
├── utils/ # Shared utilities (AppError)
└── index.ts # Entry point and server configurationEntry Point
The server initializes in src/index.ts.
It configures global middleware, sets up CORS, registers routes, attaches the error handler, and starts the HTTP server using @hono/node-server.
Key Configuration
| Option | Value |
|---|---|
| Port | 3001 (or process.env.PORT) |
| CORS | Credentials enabled, origin restricted to process.env.APP_ORIGIN |
| Route Prefix | All routes mounted under / |
// src/index.ts
import "dotenv/config";
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { errorMiddleware } from "./middlewares/error.middleware.js";
import routes from "./routes/example.router.js";
const app = new Hono();
/* ---------- global middleware ---------- */
app.use(
"/*",
cors({
origin: process.env.APP_ORIGIN || "http://localhost:3000",
credentials: true,
})
);
app.use(logger());
app.onError(errorMiddleware);
/* ---------- routes ---------- */
app.get("/health", (c) =>
c.json({ status: "ok", timestamp: new Date().toISOString() })
);
app.route("/", routes);
/* ---------- start server ---------- */
serve(
{ fetch: app.fetch, port: Number(process.env.PORT || 3001) },
(info) => console.log(`Server running on http://localhost:${info.port}`)
);Routing & Controllers
Routes are defined in src/routes and delegate to handler functions in src/controllers.
Defining Routes
Use a standard Hono instance:
import { Hono } from "hono";
import { exampleController } from "../controllers/example.controller.js";
const router = new Hono();
router.get("/", exampleController);
export default router;Controllers
Controllers are async functions that contain request-level logic.
import type { Context } from "hono";
import { AppError } from "../utils/AppError.js";
export const exampleController = async (c: Context) => {
return c.json({
status: "success",
message: "Hono API is working!",
timestamp: new Date().toISOString(),
});
};Validation
The database package exposes Zod validation schemas derived from your database models. These schemas ensure type-safe and validated inputs in your Hono controllers.
// drizzle
// src/controllers/example.controller.ts
import { db, example, insertExampleSchema } from "@light/database";
export const createExample = async (c: Context) => {
try {
const body = await c.req.json();
const validData = insertExampleSchema.parse(body);
const [newItem] = await db.insert(example)
.values(validData)
.returning();
return c.json({ status: "success", data: newItem }, 201);
} catch (err) {
throw err;
}
};// mongoose
// src/controllers/example.controller.ts
import { example, insertExampleSchema } from "@light/database";
export const createExample = async (c: Context) => {
try {
const body = await c.req.json();
const validData = insertExampleSchema.parse(body);
const newItem = await example.create(validData);
return c.json({ status: "success", data: newItem }, 201);
} catch (err) {
throw err;
}
};Error Handling
Hono centralizes all API error responses using a single global errormiddleware(src/middlewares/error.middleware.ts).
This ensures consistent, predictable, and type-safe error shapes across the API.
The Error Middleware
| Error Kind | Status | Response Body |
|---|---|---|
| ZodError | 400 | { status: "fail", errors: zod.flatten() } |
| AppError | custom | { status: "error", message } |
| Unknown | 500 | { status: "error", message: "Internal server error" } |
Using AppError
Any route, controller, or utility can throw an AppError and the middleware will reply with the supplied status code.
import { AppError } from '../utils/AppError.js';
// 404 Not Found
throw new AppError(404, "Resource not found");
// 403 Forbidden
throw new AppError(403,'You do not have permission');