02

Choose Bun and an HTTP Framework

Decision: keep Bun, adopt Hono as ParcelFlow’s HTTP layer, and preserve a framework-neutral application core.

The enterprise problem and today’s slice

Enterprise problem: A runtime or framework chosen from hype can trap the order path behind incompatible middleware, weak type evidence, or abstractions the team cannot operate, delaying customer orders and increasing migration cost.

Whole-course context: ParcelFlow already has one request-level tracer bullet, an explicit fetch boundary, and fake payment and in-memory state; today turns that evidence into a recorded platform decision.

Today’s slice: Compare raw Bun.serve, Hono, Elysia, Express-on-Bun, and a Python FastAPI counterpart, then make Hono the course default without claiming it is universal.

End-of-day evidence: A decision record, contract tests, tsc --noEmit, and a dependency compatibility probe show the same accepted and denied order behaviour through the chosen adapter.

Still unsolved: Durable PostgreSQL state, extracted services, asynchronous events, production identity, observability, and deployment remain deferred; no real payment processor is introduced.

The thesis is that runtime and HTTP framework are separate decisions: Bun owns execution and tools, while the framework should only translate HTTP into ParcelFlow commands. The smallest complete model is transport adapter, framework-neutral application, and evidence suite.

Separate the runtime choice from the framework choice

When Bun and a web framework are treated as one purchase, teams cannot isolate whether a failure comes from execution, package compatibility, routing, or application code.

The general rule is to score runtime capabilities separately from transport conveniences. The simple ParcelFlow example can run the same Fetch handler directly with Bun.serve or behind Hono. A realistic service needs routing, error normalization, validation integration, OpenAPI generation, middleware composition, support policy, and migration cost. The failure mode is selecting the most feature-rich framework before proving those features matter. The decision rule is: use the thinnest adapter that removes repeated transport work while keeping createApp(deps) independent of the framework.

OptionStrengthsCosts and risksUse whenAvoid when
Raw Bun.serveMinimal dependency surface; direct Fetch API; easiest runtime diagnosisYou own routing conventions, middleware composition, validation, error mapping, and OpenAPI integrationFew routes, unusual protocol needs, or a deliberate minimal platformMany teams need shared HTTP policy and would rebuild a framework inconsistently
Hono — course defaultSmall Fetch-based API; explicit middleware; Bun guide; portable across several runtimesExtra abstraction and dependency; schema/OpenAPI choices still need disciplineYou want a thin adapter and possible runtime portability without coupling domain codeTeam relies on framework-specific decorators or implicit container behaviour
Elysia — viable alternativeBun-first design, strong TypeScript inference, integrated schemas and lifecycleMore framework-specific types and conventions can widen migration costBun commitment is strong and integrated schema ergonomics are valuableRuntime portability or an already-established Fetch abstraction dominates
Express on Bun — migration pathFamiliar ecosystem and lower rewrite cost for an existing Express applicationNode compatibility gaps and middleware assumptions require probes; API is less Fetch-nativeMigrating an existing Express estate incrementallyGreenfield code has no ecosystem constraint and values Bun-native primitives
FastAPI / Starlette on Python with uvMature Python typing and schema ecosystem; natural fit near Python ML/data code; fast dependency workflow with uvDifferent runtime and concurrency model; cross-language ownership and packaging costsPython domain libraries or team capability are decisiveChoosing it only to mimic TypeScript routes or when one-language operations matter more

Hono documents Bun deployment directly at hono.dev/docs/getting-started/bun. Elysia presents itself as a Bun-first framework at elysiajs.com. Bun documents Express usage and compatibility at bun.sh/guides/ecosystem/express. The Python counterpart is not a loser: FastAPI builds on Starlette, and uv supplies Python project and dependency tooling. Choose from workload and team evidence, not language identity.

Implement Hono as a replaceable adapter

If Hono performs business decisions inside route callbacks, fast routing still produces a tightly coupled system that is expensive to test or migrate.

The simple adapter translates HTTP into a command and translates a typed result back into HTTP. A realistic version centralizes tenant extraction, request IDs, content limits, error envelopes, and versioning middleware while the order application remains unaware of Hono. The failure mode is passing Context into the order service. The decision rule is: framework types stop at the adapter boundary.

import { Hono } from "hono";

type Dependencies = {
  placeOrder(input: {
    tenantId: string;
    sku: string;
    quantity: number;
  }): Promise<{ orderId: string; status: "accepted" }>;
};

export function createHttpApp(deps: Dependencies) {
  const http = new Hono();

  http.post("/orders", async (context) => {
    const tenantId = context.req.header("x-tenant-id");
    const body: unknown = await context.req.json().catch(() => undefined);
    if (!tenantId || !isOrderInput(body)) {
      return context.json({ code: "invalid_order" }, 400);
    }
    const result = await deps.placeOrder({ tenantId, ...body });
    return context.json(result, 201);
  });

  return http;
}

function isOrderInput(value: unknown): value is { sku: string; quantity: number } {
  if (typeof value !== "object" || value === null) return false;
  const record = value as Record<string, unknown>;
  return typeof record.sku === "string"
    && Number.isInteger(record.quantity)
    && Number(record.quantity) > 0;
}
import { createHttpApp } from "./http";
import { placeOrder } from "./orders";

const http = createHttpApp({ placeOrder });
Bun.serve({ port: 3000, fetch: http.fetch });

Type checking and compatibility are separate gates

When code starts successfully, teams can wrongly infer it is type-safe and Node-compatible, allowing defects to reach production despite a green smoke test.

Bun’s TypeScript loader strips TypeScript syntax and executes the resulting JavaScript; the official file-type documentation explicitly says Bun does not perform type checking. Therefore ParcelFlow runs the TypeScript compiler as an independent gate. Bun also documents Node compatibility as ongoing, with some modules or behaviours not fully compatible. The simple rule is bun test plus tsc --noEmit; the realistic rule adds probes for database drivers, tracing libraries, and middleware used in production. The failure mode is testing only compilation or only happy-path execution. The decision rule is: each important claim needs its own falsifier.

bun install --frozen-lockfile
bun test
bunx tsc --noEmit
bun run scripts/probe-runtime-compatibility.ts

See Bun file types, Bun TypeScript guidance, and the current Node.js compatibility matrix. These commands define expected evidence; this lesson does not claim they were executed here.

Migrate Express by preserving the contract first

When an existing Express estate is rewritten into Hono in one release, middleware ordering and error semantics can fail together and leave no safe rollback.

The general rule is to change one axis at a time. The simple ParcelFlow migration first runs the current Express app on Bun behind its existing request contract, then moves one /orders route to a framework-neutral placeOrder function, and only then replaces the HTTP adapter with Hono. A realistic migration inventories middleware that depends on Node streams, process globals, native addons, or Express-specific response mutation and gives each a compatibility probe. The failure mode is interpreting a successful server start as proof that every production middleware path works. The decision rule is: preserve contract fixtures, canary a bounded route, and keep the old adapter selectable at the composition root until positive and negative traffic evidence matches.

const transport = process.env.HTTP_ADAPTER === "express"
  ? createExpressAdapter({ placeOrder })
  : createHonoAdapter({ placeOrder });

Bun.serve({ port: 3000, fetch: transport.fetch });

What Anthropic’s Bun evidence does and does not prove

When company adoption is repeated without scope, a distribution-tooling fact can be inflated into an unsupported claim about an AI inference backend.

Anthropic announced its acquisition of Bun on 2025-12-03 and tied the relationship to Claude Code, its native installer, developer-tooling speed, stability, and future capabilities: Anthropic acquisition announcement. Public Claude Code packaging has used Bun-compiled distribution, and the official Claude Code Action installs a pinned Bun version, runs bun install --production, and executes its TypeScript entrypoint with Bun: commit-pinned action.yml and commit-pinned repository guidance.

That evidence supports a narrow realistic example: Bun can serve demanding developer-tool distribution and automation workflows at Anthropic. The failure mode is asserting “Anthropic’s core model inference or backend runs on Bun.” The cited public evidence does not establish that. The decision rule is: distinguish runtime adoption by product surface and demand a primary source before extending the claim to a different system boundary.

Key takeaways

Without separated decision criteria, runtime enthusiasm can lock application logic to one framework and conceal compatibility debt.

  • Keep Bun as runtime and tooling choice; keep Hono as a replaceable HTTP adapter.
  • Treat Elysia as a credible Bun-first alternative and Express as a practical migration path.
  • Treat FastAPI, Starlette, and uv as a fair Python counterpart when Python capability or libraries dominate.
  • Run tsc --noEmit because Bun transpiles TypeScript but does not type-check it.
  • Cite Anthropic’s Claude Code distribution evidence narrowly; it does not prove the core inference backend uses Bun.

Checklist

If the chosen stack lacks a rollback and compatibility probe, the decision is preference rather than production evidence.

  • [ ] Record Hono as the course default and the criteria that would reverse it.
  • [ ] Keep framework types out of the order application and adapter ports.
  • [ ] Run request-contract tests for accepted and rejected orders.
  • [ ] Require bun test, tsc --noEmit, and representative dependency probes in CI.
  • [ ] Pin versions in the lockfile and record failed upgrade evidence.

Sources

When current framework and company claims are not locally cited, readers cannot distinguish verified evidence from architectural inference.