01

One Request, One Complete System

Build: ParcelFlow accepts one tenant-scoped order and returns a delivery promise from one Bun process.

The enterprise problem and today’s slice

Enterprise problem: A merchant cannot promise delivery when an order disappears between validation, payment, and inventory, and a tenant leak would turn an operational mistake into a customer trust failure.

Whole-course context: This is the first slice of ParcelFlow, the recurring order-to-delivery system; the course starts with one complete process so later service boundaries must be earned by evidence rather than assumed.

Today’s slice: Build one POST /orders request with Bun.serve, explicit validation, an in-memory repository, and a fake payment adapter; real payment networks and durable storage remain outside the boundary.

End-of-day evidence: A red-then-green Bun test and one HTTP request show a valid tenant receives an order identifier while malformed input receives a stable rejection without creating state.

Still unsolved: A process restart loses every order, concurrent reservations can oversell inventory, and no production authentication, database, message broker, or service extraction exists yet.

The thesis is simple: begin with the smallest complete system that can prove a customer outcome, then preserve seams inside it. The smallest complete model is request, application decision, and observable response.

Bun’s minimal runtime model

When a new runtime is presented as a catalogue of speed claims, developers miss the operational question: which executable owns request handling, tests, dependencies, and compatibility risk?

The general rule is to adopt the smallest runtime surface that proves the workload. In the simple ParcelFlow example, the bun executable runs TypeScript directly, Bun.serve exposes a Fetch-compatible handler, and bun test drives that same handler. In a realistic repository, the package manager and test runner may also be Bun while CI separately runs the TypeScript compiler. The failure mode is assuming “Node-compatible” means every package and edge behaviour is identical. The decision rule is: keep a compatibility test for each important dependency and choose Bun only when measured startup, throughput, or tool consolidation is worth that test burden.

Bun’s official documentation describes the runtime, package manager, bundler, and test runner as one toolkit, while also calling Node compatibility an ongoing effort. The server API accepts a fetch(Request) function, so the domain does not need to know about sockets or framework objects. See Bun overview, HTTP server, and Node.js compatibility.

TDD tracer bullet and request validation

If the first test exercises only a helper, the application can remain unassembled and the first real HTTP request can fail at the seams.

Test-driven development (TDD) means writing a failing behavioural test, making the smallest implementation pass, then improving structure without changing behaviour. A tracer bullet is a thin path through every essential layer. Start with the request-level test, not with repository classes:

import { expect, test } from "bun:test";
import { createApp } from "./app";
import { memoryInventory, memoryOrders } from "./memory";

test("accepts one tenant order end to end", async () => {
  const orders = memoryOrders();
  const inventory = memoryInventory({ "tenant-a:parcel-small": 3 });
  const app = createApp({
    orders,
    inventory,
    payment: { authorize: async () => ({ id: "pay_fake_01" }) },
  });

  const response = await app.fetch(new Request("http://local/orders", {
    method: "POST",
    headers: { "content-type": "application/json", "x-tenant-id": "tenant-a" },
    body: JSON.stringify({ sku: "parcel-small", quantity: 1 }),
  }));

  expect(response.status).toBe(201);
  expect(orders.allFor("tenant-a")).toHaveLength(1);
});

The simple validation rule rejects missing identity, unknown fields, invalid JSON, and non-positive quantity before side effects. A realistic request also has size limits, normalized SKU syntax, a request ID, and a content-type check. The failure mode is validating after payment or inventory mutation. The decision rule is: parsing and validation must finish before the first irreversible adapter call.

One-process composition and lifecycle

When startup constructs dependencies in route handlers, every request can get fresh state and shutdown cannot release resources predictably.

The general rule is to create long-lived adapters once, compose the application once, and make the server lifecycle explicit. For ParcelFlow, repositories and the fake payment adapter are created before Bun.serve; the handler closes over the composed app. A realistic service also installs readiness, graceful shutdown, structured logging, and connection cleanup. The failure mode is module-level hidden state that survives tests or differs between workers. The decision rule is: startup owns construction, request code owns use, and shutdown owns release.

const deps = {
  orders: memoryOrders(),
  inventory: memoryInventory({ "tenant-a:parcel-small": 3 }),
  payment: fakePayment(),
};
const app = createApp(deps);

const server = Bun.serve({
  port: 3000,
  fetch: app.fetch,
});

process.on("SIGTERM", () => server.stop());

The Bun test runner documentation covers its Jest-compatible API and TypeScript support. These snippets are designed to be runnable in a small project, but this lesson does not claim they were executed here.

bun test
bun run src/server.ts
curl -i -X POST http://localhost:3000/orders \
  -H 'content-type: application/json' \
  -H 'x-tenant-id: tenant-a' \
  --data '{"sku":"parcel-small","quantity":1}'

Key takeaways

Without one end-to-end slice, architecture discussions optimize imaginary boundaries instead of customer evidence.

  • Start ParcelFlow as a modular monolith: one deployment, explicit modules, replaceable adapters.
  • Validate tenant and request shape before payment, inventory, or persistence side effects.
  • Use a fake payment adapter only; a real processor is deliberately outside this course.
  • Let the request-level TDD tracer bullet prove response and owned state together.
  • Treat Bun compatibility and type checking as evidence obligations, not assumptions.

Checklist

If any proof remains implicit, the next refactor can break the customer path without a useful signal.

  • [ ] Write the failing request test before the handler implementation.
  • [ ] Confirm the valid path returns 201 and stores exactly one tenant order.
  • [ ] Confirm malformed input and fake decline create no order or reservation.
  • [ ] Keep HTTP, application, payment, inventory, and repository responsibilities named.
  • [ ] Record that restart durability, concurrency, and real payments are still deferred.

Sources

When runtime facts lack primary citations, rapidly changing compatibility and API details can turn a useful lesson into stale guidance.