Design specifications
A design exploration: a thin modular-monolith convention for Patties, built directly on Bun-native primitives. A module is a folder with a public index.ts, wiring is a plain import, and the only new machinery is a build-time public-API boundary check plus a route-merge macro. No DI container, no decorators, no NestJS clone. The Release page tracks each spec's status.
The thesis in one line
A modular monolith is one deployable, partitioned into modules that communicate only through their public surface. That boundary is the whole value — everything else is ceremony Bun lets us delete. Bun's module graph already is a dependency graph; we lean on it and add one build-time rule: a module may only reach another module's public index.ts.
Intended design only
This section is a design exploration. The modular-monolith convention described here is proposed, not yet implemented — see the delivery tracker for status. The shipped framework behaviour is documented under Docs.
Deliberately thin
The goal is structure, not a framework that owns your code. Reach for a Bun or web primitive every time one exists.
index.ts), a boundary check (build error on deep imports), a discovery macro (Bun.Glob merges routes at build), and a scaffolder.
reflect-metadata, no guards / pipes / interceptors / filters, no new validation framework. Wiring is plain ES import; request flow is compose().
If you delete the framework, your modules are still just folders of plain functions. That's the test for "thin."
A module is a folder
Everything inside is private by default. The index.ts is the contract — the only thing other modules are allowed to import.
app/
billing/
index.ts ← public API (the front door)
routes.ts ← HTTP surface: (req, ctx) => Response
invoice.service.ts ← internal
invoice.schema.ts ← internal (Zod, if you want it)
orders/
index.ts
routes.ts
order.service.ts ← imports billing ONLY via ../billing
// app/billing/index.ts — the public surface (and the ONLY import target)
export { invoiceService } from "./invoice.service";
export type { Invoice } from "./invoice.service";
// app/orders/order.service.ts — cross-module wiring is just an import
import { invoiceService } from "../billing"; // ✅ through the public index
// import { ledger } from "../billing/invoice.service" // ❌ build error: internal
Key rules
- RULE 1Public API or nothing. A module may only import another module's
index.ts. Deep imports into internals are a build error withfile:line. - RULE 2Wiring is a plain import. Bun's module graph is the dependency graph. No container, no tokens, no
reflect-metadata. - RULE 3Discover at build, serve frozen. A macro finds modules and inlines the merged route table. The production server runs
Bun.serve({ routes })and scans nothing. - RULE 4Standards at the boundary. Handlers take a standard
Request+ a thinctxand return aResponse. Request flow is the existingcompose()middleware. - RULE 5Bun-native only.
Bun.serve,Bun.Glob,Bun.build,bun --hot. No Express, no chokidar, no Webpack. - RULE 6Libraries stay libraries. Validation, ORM, jobs — call them in a handler. The framework doesn't wrap them in phases or providers.