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.

Customer use cases

When a platform choice is disconnected from customer work, benchmark wins can hide broken validation or migration costs that delay actual orders.

Use case IDActorCustomer jobSuccess outcomeDenial or recovery evidence
D02-UC-01Tenant checkout clientSubmit the same order through the selected HTTP adapterHono on Bun returns the established 201 contract without changing the application coreInvalid input still returns the established 400 response and creates no order
D02-UC-02ParcelFlow platform maintainerChoose and upgrade a supported runtime/framework combination with evidenceA versioned decision record compares alternatives and CI checks types, tests, and a representative dependencyA failed compatibility probe blocks adoption while the raw Fetch adapter remains a recovery path

Actor-centred user stories

Without actor-centred acceptance conditions, “developer experience” becomes an untestable preference and customer-visible compatibility regressions arrive late.

Story IDUse case IDsUser storyObservable acceptance conditions
D02-US-01D02-UC-01As a tenant checkout client, I want framework changes to preserve the order API, so that checkout behaviour does not vary with internal toolingValid request remains 201, invalid request remains 400, response schema is unchanged, and repository state matches Day 01
D02-US-02D02-UC-02As a ParcelFlow platform maintainer, I want an evidence-based runtime and framework decision, so that upgrades are reversible and type errors cannot ship silentlyDecision record names criteria and alternatives; tests, tsc --noEmit, and a Node-compatibility probe are required; failure keeps the prior adapter deployable

End-to-end product flows

If framework evaluation stops at a hello-world route, middleware and error translation failures remain invisible until a tenant uses checkout.

Flow IDUse case IDsPathTriggerNumbered stepsTerminal evidence
D02-FLOW-01D02-UC-01, D02-UC-02HappyCheckout posts a valid order through the Hono adapter on Bun1. Hono matches the route.<br>2. Adapter validates tenant and body.<br>3. Framework-neutral application executes.<br>4. Adapter maps the result to 201.<br>5. CI runs request tests and type checking.Stable HTTP body, one stored test order, green contract test, and zero tsc --noEmit diagnostics
D02-FLOW-02D02-UC-01, D02-UC-02RecoveryUpgrade or migrated Express middleware breaks the contract or an unsupported Node API1. Compatibility test fails.<br>2. CI blocks the change.<br>3. Maintainer reverts the adapter or routes through raw Fetch.<br>4. Re-run valid and invalid controls.Failed probe identifies package and version; prior adapter returns 201/400 controls without data mutation on denial

System design derived from the flows

When framework objects enter domain code, changing Hono, Elysia, or Express forces a risky rewrite of order rules rather than a bounded adapter change.

Use case IDEntry pointResponsible servicesAuthoritative storeFailure evidence
D02-UC-01Hono POST /orders route served by BunHono HTTP adapter and framework-neutral order applicationIn-memory order repository owned by order moduleContract mismatch, 400/500 response, or state mutation after invalid input
D02-UC-02Pull request and CI workflowPlatform decision record, Bun runtime, TypeScript compiler, Bun test runner, compatibility probeGit owns the decision and lockfile; CI owns immutable run outputType diagnostic, test failure, dependency probe error, or diff from baseline response

Data model and ownership

If a technology decision has no owned record or reproducible evidence, teams repeat the debate and cannot explain why a risky upgrade was accepted.

Generated-application database: Not created in this slice — ParcelFlow still uses the disposable in-memory order repository; Git and CI retain the framework decision and its reproducible evidence.

Record or entityStore and ownerPrimary keyForeign key or opaque referenceTenant keyMaterial constraintLifecycle and deletionUse case IDs
HTTP contract fixtureGit repository owned by ParcelFlow application teamfixture_name plus contract versionOpaque reference to test case nametenant_id appears only as synthetic fixture dataAccepted and rejected response shapes are versioned together; no secrets or real customer dataCreated with route contract, revised by review, retained with source history, deleted only after supported version retirementD02-UC-01, D02-UC-02
Runtime decision recordGit repository owned by platform maintainersdecision_idOpaque CI run URL and lockfile commitNone — decision applies to all tenants and contains no tenant dataRecords criteria, chosen versions, alternatives, rollback, and known compatibility gapsAccepted by review, superseded rather than overwritten, retained with repository historyD02-UC-02
Compatibility resultCI artifact store owned by platform maintainersImmutable run_idOpaque reference to commit SHA and decision IDNone — synthetic probes contain no tenant recordsTest, type-check, and dependency-probe status must all be recordedProduced per change, retained under CI policy, expires without changing the source decisionD02-UC-01, D02-UC-02

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.