Philosophy
1. If every app needs it, it is not a brick
Section titled “1. If every app needs it, it is not a brick”Express asked you to assemble a working server from body-parser, cookie-parser, multer,
cors and morgan. That was never a real choice. Nobody sat down and decided their API should
not parse JSON bodies — they just had to remember, in the right order, forever.
Oven ships those behaviours in core. Always on. Nothing to install, nothing to register, nothing to order, nothing to forget.
The rule we apply: if it appears in more than 80% of real apps, it goes in core with no switch. Things that genuinely vary — CORS policy, rate limits, compression — stay configurable, but they still ship in the box. Configured, not installed.
// Expressapp.use(express.json())app.use(cookieParser())app.use(multer().single('file'))
// Oven — this is the whole fileexport default async (ctx) => { const { name } = await ctx.body // JSON, form or multipart, whichever arrived const files = await ctx.files() // uploads, as web File objects ctx.cookies.set('seen', '1', { httpOnly: true }) return { name, uploaded: Object.keys(files) }}What you lose is the ability to not have those. In eleven years of Express nobody wanted that. What you gain is that they cannot be forgotten, cannot be ordered wrongly, and cannot be a different version in two services.
2. Always-on must be lazy
Section titled “2. Always-on must be lazy”Batteries included is worthless if you pay for batteries you never use.
Nothing in Oven is computed until it is read. A route that returns a string never parses a body, never parses the query string, never touches cookies, never generates a request id, and never derives a logger.
app.get('/ping', () => 'pong')// no URL parsed, no query parsed, no cookies read, no request id generated, no logger builtEvery one of those is a getter that caches on first touch. ctx.path does not even construct a
URL — it scans the string, because new URL() on every request costs more than the routing does.
This is enforced by tests rather than good intentions: the suite asserts that reading the URL does not materialise a request id, and that setting a header does not either. Those tests exist because the property is invisible — nothing about a passing request tells you what it didn’t do, so the only way to keep it true is to assert it.
3. Fresh start. We owe the past nothing.
Section titled “3. Fresh start. We owe the past nothing.”Oven is designed in 2026 for 2026. There is no Express compatibility layer, no
(req, res, next) signature, no Connect middleware support, no CommonJS build, no callback
API, and no Node stream interop.
Every framework that offered an Express shim ended up shaped by Express, inheriting its mistakes along with its users. We would rather be honest: if you want Express, Express exists and is very good at being Express.
What this means in practice:
- ESM only. No
require, no dual build. - Web standards only.
Request,Response,Headers,URL,FormData,ReadableStream. - Async only. Every extension point returns a promise or a value. Nothing takes a callback.
- Bun only.
Bun.serve,Bun.file,Bun.S3Client,bun:sqlite,bun:test.
4. Types are the documentation
Section titled “4. Types are the documentation”If you need to read docs to discover that a property exists, we failed. Autocomplete on ctx
should teach you the framework.
This is why bricks contribute their types through registration: add storage and ctx.storage
appears, fully typed. Leave it out and touching ctx.storage is a compile error, not a
runtime crash at 3am.
const app = createApp().use(db(drizzleSqlite({ schema })))
app.get('/users', (ctx) => ctx.db.select().from(users)) // ✓ typed from your schemaapp.get('/files', (ctx) => ctx.storage.list()) // ✗ compile error: no storage brickIt is also why ctx.db is the Drizzle client rather than something of ours wrapped around it. An
invented query API would be permanently behind every ORM, worse than all of them, and unknown to
every coding model — which knows Drizzle and Prisma cold.
The same rule explains an absence: there is no ctx.state bag for middleware to stash things in.
An untyped bucket is invisible to autocomplete and unknown at the point you read it. State that
belongs on the context belongs to a brick, where it arrives
typed.
5. Errors are values, not accidents
Section titled “5. Errors are values, not accidents”A handler either returns a value or throws. Throwing NotFound produces a correct RFC 9457
application/problem+json response, whether the throw was synchronous or not.
export default async (ctx) => { const user = await find(ctx.params.id) if (!user) throw new NotFound(`No user ${ctx.params.id}.`) return user}{ "type": "about:blank", "title": "Not Found", "status": 404, "detail": "No user 8f14.", "instance": "/users/8f14" }Errors we did not plan for are treated as bugs: they become a 500, and their message and stack are withheld in production. An unplanned error message is exactly where connection strings and internal hostnames leak.
There is no next(err), and no express-async-errors to remember. A rejected promise from a
handler, a middleware, or a hook is caught the same way a synchronous throw is.
6. Two implementations, or it is not a contract
Section titled “6. Two implementations, or it is not a contract”Anywhere Oven meets something outside itself — a database, an auth provider, a mail service — the
shape is a small contract package plus many adapters, with a raw escape hatch and capabilities
declared at boot rather than discovered at runtime.
The test of a contract is whether a second, structurally different implementation fits it
without changes. db-drizzle alone proves nothing; db-drizzle plus db-mongoose proves it —
and it was Mongoose that showed transaction could not be portable, so it became optional and
declared rather than faked. The same reason the validator contract is exercised with Valibot and
not only Zod.
A contract must never invent. Where two providers genuinely disagree — Clerk hosts its sign-in page, better-auth mounts routes — the contract exposes the difference instead of giving one of them a method to fake.