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 ID | Actor | Customer job | Success outcome | Denial or recovery evidence |
|---|---|---|---|---|
| D02-UC-01 | Tenant checkout client | Submit the same order through the selected HTTP adapter | Hono on Bun returns the established 201 contract without changing the application core | Invalid input still returns the established 400 response and creates no order |
| D02-UC-02 | ParcelFlow platform maintainer | Choose and upgrade a supported runtime/framework combination with evidence | A versioned decision record compares alternatives and CI checks types, tests, and a representative dependency | A 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 ID | Use case IDs | User story | Observable acceptance conditions |
|---|---|---|---|
| D02-US-01 | D02-UC-01 | As a tenant checkout client, I want framework changes to preserve the order API, so that checkout behaviour does not vary with internal tooling | Valid request remains 201, invalid request remains 400, response schema is unchanged, and repository state matches Day 01 |
| D02-US-02 | D02-UC-02 | As a ParcelFlow platform maintainer, I want an evidence-based runtime and framework decision, so that upgrades are reversible and type errors cannot ship silently | Decision 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 ID | Use case IDs | Path | Trigger | Numbered steps | Terminal evidence |
|---|---|---|---|---|---|
| D02-FLOW-01 | D02-UC-01, D02-UC-02 | Happy | Checkout posts a valid order through the Hono adapter on Bun | 1. 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-02 | D02-UC-01, D02-UC-02 | Recovery | Upgrade or migrated Express middleware breaks the contract or an unsupported Node API | 1. 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 ID | Entry point | Responsible services | Authoritative store | Failure evidence |
|---|---|---|---|---|
| D02-UC-01 | Hono POST /orders route served by Bun | Hono HTTP adapter and framework-neutral order application | In-memory order repository owned by order module | Contract mismatch, 400/500 response, or state mutation after invalid input |
| D02-UC-02 | Pull request and CI workflow | Platform decision record, Bun runtime, TypeScript compiler, Bun test runner, compatibility probe | Git owns the decision and lockfile; CI owns immutable run output | Type 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 entity | Store and owner | Primary key | Foreign key or opaque reference | Tenant key | Material constraint | Lifecycle and deletion | Use case IDs |
|---|---|---|---|---|---|---|---|
| HTTP contract fixture | Git repository owned by ParcelFlow application team | fixture_name plus contract version | Opaque reference to test case name | tenant_id appears only as synthetic fixture data | Accepted and rejected response shapes are versioned together; no secrets or real customer data | Created with route contract, revised by review, retained with source history, deleted only after supported version retirement | D02-UC-01, D02-UC-02 |
| Runtime decision record | Git repository owned by platform maintainers | decision_id | Opaque CI run URL and lockfile commit | None — decision applies to all tenants and contains no tenant data | Records criteria, chosen versions, alternatives, rollback, and known compatibility gaps | Accepted by review, superseded rather than overwritten, retained with repository history | D02-UC-02 |
| Compatibility result | CI artifact store owned by platform maintainers | Immutable run_id | Opaque reference to commit SHA and decision ID | None — synthetic probes contain no tenant records | Test, type-check, and dependency-probe status must all be recorded | Produced per change, retained under CI policy, expires without changing the source decision | D02-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.
| Option | Strengths | Costs and risks | Use when | Avoid when |
|---|---|---|---|---|
Raw Bun.serve | Minimal dependency surface; direct Fetch API; easiest runtime diagnosis | You own routing conventions, middleware composition, validation, error mapping, and OpenAPI integration | Few routes, unusual protocol needs, or a deliberate minimal platform | Many teams need shared HTTP policy and would rebuild a framework inconsistently |
| Hono — course default | Small Fetch-based API; explicit middleware; Bun guide; portable across several runtimes | Extra abstraction and dependency; schema/OpenAPI choices still need discipline | You want a thin adapter and possible runtime portability without coupling domain code | Team relies on framework-specific decorators or implicit container behaviour |
| Elysia — viable alternative | Bun-first design, strong TypeScript inference, integrated schemas and lifecycle | More framework-specific types and conventions can widen migration cost | Bun commitment is strong and integrated schema ergonomics are valuable | Runtime portability or an already-established Fetch abstraction dominates |
| Express on Bun — migration path | Familiar ecosystem and lower rewrite cost for an existing Express application | Node compatibility gaps and middleware assumptions require probes; API is less Fetch-native | Migrating an existing Express estate incrementally | Greenfield code has no ecosystem constraint and values Bun-native primitives |
FastAPI / Starlette on Python with uv | Mature Python typing and schema ecosystem; natural fit near Python ML/data code; fast dependency workflow with uv | Different runtime and concurrency model; cross-language ownership and packaging costs | Python domain libraries or team capability are decisive | Choosing 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
uvas a fair Python counterpart when Python capability or libraries dominate. - Run
tsc --noEmitbecause 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.