Building Backends with Bun and Hono Without Making Them Heavy
A practical way I like to structure Bun and Hono backends: small routes, typed validation, clear errors, and no unnecessary framework weight.
I like backends that stay boring for a long time. Not boring as in weak. Boring as in predictable. A request comes in, validation happens, business logic runs, the response is shaped clearly, and errors do not leak random details.
Bun and Hono fit that style very well. Bun gives the runtime, package manager, test runner, and scripts in one place. Hono gives a small web framework that does not force a huge folder structure on top of the app.
The structure I prefer
For most APIs, I like this kind of shape:
src/
app.ts
index.ts
routes/
index.ts
subscriptions.ts
services/
subscription.service.ts
db/
index.ts
schema/
subscribers.ts
middleware/
error.ts
The important part is not the exact folder names. The important part is that route files do routing, service files do product logic, and database files do database work.
When those boundaries are clear, changing one part does not disturb everything else.
Keep route files thin
I do not like route files that become mini applications. A route should validate input, call the correct function, and return the response.
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
const app = new Hono();
const subscribeSchema = z.object({
email: z.string().email(),
});
app.post("/subscriptions", zValidator("json", subscribeSchema), async c => {
const body = c.req.valid("json");
const result = await subscribe(body.email);
return c.json({
success: true,
data: result,
});
});
export default app;
This is easy to scan. The schema is visible. The response shape is visible. The actual subscription behavior can be tested separately.
Mount routes explicitly
Hono’s app.route() pattern keeps bigger apps clean. I prefer it because the root app shows the public API surface in one place.
import { Hono } from "hono";
import subscriptions from "./routes/subscriptions";
const app = new Hono();
app.route("/api/v1", subscriptions);
export default app;
This avoids the “where is this endpoint coming from?” problem. When an API grows, that clarity matters.
Error handling first
Backend code becomes messy when errors are added at the end. I like defining the error response shape early.
type ApiErrorCode = "VALIDATION_ERROR" | "NOT_FOUND" | "CONFLICT" | "INTERNAL_ERROR";
interface ApiError {
success: false;
error: {
code: ApiErrorCode;
message: string;
};
}
Every endpoint should return predictable errors. If the frontend knows the shape, it can show the right message without guessing.
Do less in middleware
Middleware is powerful, so it is easy to abuse. I keep middleware for concerns that are truly cross-cutting:
- Request IDs
- Logging
- CORS
- Auth session parsing
- Rate limits
- Error formatting
I avoid putting product behavior in middleware. If business logic hides there, the request flow becomes harder to understand.
Why Bun feels good here
Bun’s biggest advantage for me is not just speed. It is the small workflow.
One runtime. One package manager. One test runner. One lockfile.
For small and medium backends, that removes a lot of setup noise. I can create an endpoint, write tests, run scripts, and ship without thinking about ten tools.
My backend checklist
Before I call a backend ready, I check these things:
- Can I read the route list quickly?
- Are request bodies validated at the boundary?
- Do errors have one response shape?
- Are database queries hidden behind service or repository functions?
- Is the environment config typed?
- Can the app boot without connecting to external services too early?
- Do tests cover the risky behavior?
This checklist is simple, but it catches a lot.
Final thought
The best backend stack is the one that stays understandable after the first month. Bun and Hono make that possible because they do not add much ceremony.
But the stack alone will not save the codebase. Thin routes, typed validation, clear errors, and boring boundaries are still the real work.