Batteries included
Here is a working Express app that parses JSON, reads cookies, accepts file uploads, logs requests, and does not crash on an async error:
const express = require('express')const cookieParser = require('cookie-parser')const multer = require('multer')const morgan = require('morgan')require('express-async-errors')
const app = express()app.use(express.json())app.use(express.urlencoded({ extended: true }))app.use(cookieParser(process.env.COOKIE_SECRET))app.use(morgan('combined'))const upload = multer({ dest: '/tmp' })
app.post('/avatar', upload.single('file'), async (req, res, next) => { // …and you still have to remember `next(err)` in anything that predates the shim})Five dependencies, six lines of registration, and an ordering you have to get right — json()
before your routes, cookieParser before anything reading a cookie. None of that was ever a
decision. It was homework.
Here is the same thing in Oven:
export default defineRoute( { body: z.object({ file: z.file().max(5_000_000) }) }, async (ctx) => ctx.storage.upload(`avatars/${ctx.user.id}`, ctx.body.file),)What is always on
Section titled “What is always on”Nothing to install, nothing to register, nothing to order.
| You get | You no longer install | |
|---|---|---|
| Body parsing | ctx.body, content-type aware: JSON, urlencoded, multipart, text, raw |
body-parser, express.json() |
| File uploads | web File objects, size and MIME limits, temp-file spill for large ones |
multer |
| Cookies | ctx.cookies.get/set/delete, signed cookies, secure defaults |
cookie-parser |
| Query parsing | ctx.query, with arrays and nested keys |
qs |
| Token capture | ctx.token from Authorization, the token cookie, or ?access_token=, in that order |
hand-rolled, every time |
| Request id + logging | ctx.id, ctx.log — structured and request-scoped |
morgan + uuid |
| Errors | thrown errors become RFC 9457 problem+json; async errors caught | express-async-errors |
| Graceful shutdown | SIGTERM drains in-flight requests, then closes brick resources | hand-rolled, every time |
The rule: if it appears in more than 80% of real apps, it goes in core with no switch.
Things that genuinely vary by app — CORS policy, rate limits, compression, security headers — still ship in the box, but as middleware you configure. Configured, not installed.
Always on, never paid for
Section titled “Always on, never paid for”Batteries included is worthless if you pay for batteries you do not use.
Nothing above is computed until you touch it. A route that returns a string parses no body,
splits no cookie header, parses no query string, and generates no request id. ctx.body is a
getter; so is ctx.query; so is ctx.cookies.
Why not middleware?
Section titled “Why not middleware?”Because app.use(cookieParser()) is a bug in framework design, not a feature.
It is a line every application writes, in the same place, for the same reason. Making it optional buys nobody anything: the framework still has to document it, every tutorial still has to include it, and the only outcomes are “you wrote it” and “you forgot and something broke oddly”. A choice where one branch is always wrong is not a choice.
The cost of getting this wrong compounds. Express’s ordering rules exist because middleware is a list, and a list has an order someone must know. Oven has no such list for these behaviours — there is nothing to order because there is nothing to register.
Where to go next
Section titled “Where to go next”- Always-on batteries — the full API for each of the above
- Middleware & hooks — the configurable half: CORS, rate limits, compression, security headers
- Bricks — the other kind of feature: add one, get a capability on the context
- Coming from Express — a line-by-line translation