Engineering

Stop Shipping Bugs: Build Type-Safe APIs with Bun + Hono

I spent two years writing Rust just to answer one question: can I bring that discipline into TypeScript? Here's what I found.

Code editor showing Bun and Hono type-safe API routes
Thomi Jasir · Feb 10, 2025
#bun#hono#typescript#backend#type-safety#rust

Working at a bank sounds impressive on paper. In reality, it made me feel like my skills were quietly rusting away. And not the good kind of Rust.

So I started writing Rust for personal projects. For more than two years, that was my escape. And for the first time in my career, I felt genuinely disciplined while writing code.

The compiler wouldn’t let me get away with anything sloppy. Every decision had to be explicit, every contract had to be clear. It was hard. But it was honest.

Then came the question I couldn’t shake.

The Question That Wouldn’t Leave My Head

JavaScript is easy. JavaScript is everywhere. But JavaScript lets you do the same thing in a hundred different ways. And that freedom is its biggest flaw.

Different teammates write the same logic in completely different styles. Consistency disappears. Discipline disappears. And eventually, undefined starts showing up in places it has absolutely no business being.

After two years of Rust, I knew what I was missing: a language that forces you to think carefully. But Rust’s strictness also makes development slow. For a chat app, a small CRUD service, or a fast-moving internal tool, Rust is overkill.

So I started asking myself:

Why don’t we take Rust’s strictness and apply it in TypeScript?

That question lived in my head for an entire week. Then I found the answer.

The Stack: Bun + Hono + Rust-Style Discipline

After research and a lot of experimentation, I landed on Bun + Hono, with configuration patterns borrowed directly from how I structure Rust projects.

This isn’t just a “faster Node.js” story. It’s about bringing real discipline to TypeScript backend development.

Why Bun Changed Everything

Bun is a JavaScript/TypeScript runtime, similar to Node.js, but written in Zig. It’s blazing fast, and more importantly, it changes the feedback loop in ways that actually compound over time.

Cold starts are gone. Our internal services spin up in under 50ms. You make a change, you see the result. Milliseconds, not seconds.

The built-in test runner (bun:test) is Jest-compatible and requires zero configuration. No extra packages, no setup dance. For a service with 40+ route handlers, that was the real win.

And TypeScript is first-class. Bun runs .ts files natively, no separate transpile step, no ts-node. Your source and your runtime are the same thing.

Why Hono?

Hono is a small (~12KB) web framework built for structured, predictable patterns. It runs on Bun, Deno, Cloudflare Workers, and Node. Same code, different targets. The API surface is intentionally minimal:

import { Hono } from "hono";

const app = new Hono();

app.get("/health", (c) => c.json({ status: "ok" }));

export default app;

But where Hono earns its place is the validator middleware. Combined with Zod, you get route-level input validation and automatic TypeScript narrowing:

import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const app = new Hono();

const createUserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
  role: z.enum(["admin", "member", "viewer"])
});

app.post("/users", zValidator("json", createUserSchema), async (c) => {
  const body = c.req.valid("json");
  // body is fully typed: { name: string, email: string, role: 'admin' | 'member' | 'viewer' }
  const user = await createUser(body);
  return c.json(user, 201);
});

Invalid requests get a 400 with a structured error before your handler even runs. No manual if (!req.body.email) checks scattered everywhere. This is what I meant by borrowing from Rust: explicit contracts, enforced by the tool, not by discipline alone.

End-to-End Type Safety with Hono RPC

Here’s where things get genuinely exciting. Hono has an RPC mode that lets you share route types directly with your frontend:

// server/routes/users.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const users = new Hono()
  .get("/", async (c) => {
    const users = await db.user.findMany();
    return c.json(users);
  })
  .post("/", zValidator("json", createUserSchema), async (c) => {
    const body = c.req.valid("json");
    const user = await db.user.create({ data: body });
    return c.json(user, 201);
  });

export type UsersRoute = typeof users;
export default users;
// client/api.ts
import { hc } from "hono/client";
import type { UsersRoute } from "../server/routes/users";

const client = hc<UsersRoute>("http://localhost:3000");

// Fully typed. TypeScript knows the request body shape and response type.
const res = await client.users.$post({
  json: { name: "Thomi", email: "thomi@venobi.com", role: "admin" }
});
const user = await res.json(); // typed as User

If the server changes the schema, say role gains a new value 'superadmin', the client code fails to compile until it’s updated. The contract is enforced at build time, not discovered at 2am when production alerts fire.

This is what I was looking for. This is the Rust discipline I missed, but in TypeScript.

The Numbers That Actually Matter

Raw HTTP benchmarks are mostly noise. Here’s what changed in daily engineering:

Benchmark result

The test suite speed is the one that changes how you actually work. When tests run in under 2 seconds, you run them constantly. When they take 8 seconds, you batch changes and run them less often. That habit is exactly where bugs hide.

What I’d Do Differently

Don’t migrate everything at once. I started by building new internal tools in Bun + Hono while leaving existing services alone. That let me learn the patterns without risking anything critical.

Colocate your Zod schemas with your routes. I initially put schemas in a separate schemas/ directory. It felt organized but created friction. Now schemas live next to the routes that use them. When you read a route, you see the contract immediately.

Use app.route() once you have more than ~8 routes. Organize into sub-apps and mount them:

const app = new Hono();
app.route("/users", usersRouter);
app.route("/projects", projectsRouter);
app.route("/auth", authRouter);

Clean, composable, and each sub-router can have its own middleware.

Q&A

Q: Do I need to be a Rust developer to use this stack? A: Not at all. The Rust connection is just about mindset. Bun and Hono are straightforward TypeScript. If you’ve used Express or Fastify before, you’ll feel at home within an hour.

Q: Is Bun production-ready? A: Yes. Bun 1.0 was released in 2023 and it’s been stable. We’ve been running it in production for over a year at this point with no issues. The ecosystem is still maturing, but for backend APIs it’s solid.

Q: What if my team is already on Node.js and Express? A: You don’t have to migrate everything. Start with a new service or internal tool. Run it alongside your existing stack. Let the team feel the difference before committing to a full switch.

Q: Does Hono RPC work with any frontend framework? A: Yes. The hc client is just a typed HTTP client. It works with React, Vue, Svelte, plain JavaScript, or anything that can make HTTP requests. The types live in your shared codebase and get imported where needed.

Q: What’s the learning curve like? A: Honestly, lower than I expected. The hardest part is getting used to writing Zod schemas for everything. Once that clicks, the rest falls into place naturally. Give it a weekend project and you’ll get it.

Conclusion

I spent two years in Rust learning what good engineering discipline feels like. Then I asked: can I bring that to TypeScript without giving up speed?

The answer is yes, but only if you pick a stack that enforces the discipline for you. Not through willpower. Not through code reviews alone. Through tools that make the wrong thing harder to do than the right thing.

Bun + Hono does that. It’s not Rust. It’s not pure JavaScript. It’s something in between. And that balance is exactly what I was looking for.

Comments

Loading comments...