Body parsing
Content-type aware, size-limited, and lazy — a route that never reads ctx.body never parses it.
Auth, a database, file storage, email and background jobs — configured, not
wired. Every one of them typed, on the request context, documented at
/docs.
import { z } from 'zod'
import { route } from '../../../route'
export default route(
{
auth: true,
params: z.object({ id: z.uuid() }),
body: z.object({ file: z.file().max(5_000_000) }),
},
async (ctx) => {
// every one of these is typed, validated and guarded
const { key } = await ctx.storage.upload(`avatars/${ctx.params.id}`, ctx.body.file)
await ctx.queue.dispatch(resizeAvatar, { key })
return ctx.db.update(users).set({ avatar: key }).where(eq(users.id, ctx.user.id))
},
)
import { createApp, loadRoutes } from '@theoven/core'
import { auth } from '@theoven/auth'
import { basicAuth } from '@theoven/auth-basic'
import { db } from '@theoven/db'
import { drizzleSqlite } from '@theoven/db-drizzle'
import { mail, resendMail } from '@theoven/mail'
import { s3Storage, storage } from '@theoven/storage'
import { queue, redisQueue } from '@theoven/queue'
export const app = createApp()
.use(db(drizzleSqlite({ url: './data.db', schema })))
.use(storage(s3Storage({ bucket: 'uploads' })))
.use(queue(redisQueue(), { jobs: [resizeAvatar] }))
.use(mail(resendMail({ apiKey, from })))
.use(auth(basicAuth({ db: client, secret })))
// eight auth endpoints, an OpenAPI document and /docs — all mounted
await loadRoutes(app, `${import.meta.dir}/routes`)
import type { Brick } from '@theoven/core'
// A brick is a function returning a descriptor. That is the whole contract.
export function clock(): Brick<'clock', { now(): Date }> {
return {
name: 'clock',
setup: () => ({ now: () => new Date() }),
}
}
const app = createApp().use(clock())
app.get('/time', (ctx) => ({ now: ctx.clock.now() }))
// ^ typed, because setup's return flowed through .use()
Validated, authenticated, typed and documented at /docs — with nothing wired
by hand. Leave a brick out and touching it is a compile error, not a crash at 3am.
Always on
Express made you assemble a working server from parts. That was never a real choice — it was homework. If more than 80% of apps need it, Oven ships it in core; the rest ship in the box, configured rather than installed.
Content-type aware, size-limited, and lazy — a route that never reads ctx.body never parses it.
Uploads arrive as web File objects, streamed rather than buffered, with size and MIME limits.
ctx.cookies with secure defaults and signing built in. No secret to remember to set.
ctx.token from the Authorization header, a cookie, or the query string — even with no auth brick.
A throw becomes an RFC 9457 response, whether it happened synchronously or three awaits deep.
replaces express-async-errorsenv.port(), env.bool(), env.duration() — parsing that throws instead of guessing, with secrets redacted from errors.
SIGTERM drains in-flight requests. Mid-drain arrivals get 503 with Retry-After, not a dead socket.
Correct preflights, Vary: Origin, and the headers every app should send. Configured, not installed.
Structured, request-scoped ctx.log with an id on every line — and on every error response.
All of it lazy: a route that returns a string parses no body, splits no cookie header and generates no request id. That is asserted by tests, not promised. See what is always on →
Add a brick, get a feature
Every brick contributes a capability to the request context, fully typed — and disappears from the type when you remove it. Each page lists what it cannot do as plainly as what it can.
Drizzle over bun:sqlite, Postgres or MySQL, and Mongoose. ctx.db is the native client — your queries stay yours.
Email and password, Google and GitHub, or Clerk and better-auth. Eight endpoints, argon2id, revocable sessions, rate limited by default.
Sign in with Google in 20 min →S3, R2, MinIO, Bunny and ImageKit, with presigned direct uploads — and a local directory so uploads work before you have a bucket.
five drivers →Typed jobs, retries with backoff, a dead letter and cron. In memory, in Redis, or in the Postgres you already run.
queue →Console driver by default, so password reset works the moment you create an app. Resend, SES or SMTP when you are ready.
mail →WebSockets upgraded from an ordinary guarded route, so auth: true is checked before a socket exists. Server-sent events for the one-way case.
Tag invalidation and stampede protection, in memory or Redis. OpenTelemetry spans named by route pattern, not by path.
cache, telemetry →The same schemas that validate generate an OpenAPI 3.1 document and a browsable reference — and filter the response, so a column you forgot to omit cannot be sent.
openapi →Bricks are not middleware. CORS, rate limiting and compression wrap a request; a brick puts something new on the context. Write your own in about fifty lines. Browse the catalogue →
Three ways in
Not a reference and a hello-world. Every snippet on the site is executed before it is published — 458 of them, checked on every commit, so a sample that drifts from the framework fails the build rather than your afternoon.
A route, then validation, errors, a database, auth. Start here if you have not written an Oven app before.
Your first route →A finished feature you drop into an app you already have. Sign in with Google, image uploads, expiring share links, real-time notifications.
Browse recipes →A whole backend in order — a chat app, a Trello clone, a video platform — including the parts that are actually hard.
Build something real →The hard parts get their own chapters. Cursor pagination, because offset shows a reader the same message twice. Card ordering, measured to the exact drag where floating-point positions collapse — 42. An upload pipeline with a video that is honestly not watchable yet. Read the ordering chapter →
Measured, not claimed
Real sockets, 50 connections, identical routes, each framework on the runtime it is actually deployed on. Every number here comes from a script in the repo.
Where it sits
| Express | Fastify | NestJS | FastAPI | Oven | |
|---|---|---|---|---|---|
| Runtime | Node | Node | Node | Python | Bun |
| Auth / DB / queue / mail / S3 | — | — | partial | partial | ✓ |
| Automatic OpenAPI | — | plugin | plugin | ✓ | ✓ |
| Types inferred from schemas | — | partial | partial | ✓ | ✓ |
| File-based routing | — | — | — | — | ✓ |
| Body / cookies / uploads always on | — | partial | ✓ | ✓ | ✓ |
| Per-request dependency injection | — | — | ✓ | ✓ | ✓ |
| Google / GitHub sign-in in the box | — | — | module | — | ✓ |
We are not competing on router microseconds — Bun makes everything fast. We compete on the
time between bun create and an app shaped like production.
Where FastAPI is still ahead: OAuth2 scopes, static file serving, and per-exception handlers.
Those are on the list rather than in the box, and the
brick pages say so.
A deliberate break
Every framework that shipped an Express compatibility layer ended up shaped by Express — inheriting its mistakes along with its users.
So Oven ships none. No (req, res, next), no Connect middleware, no CommonJS
build, no callbacks. Web standards, ESM and async only. If you want Express, Express
exists, and it is very good at being Express.
Scaffold a project and you have a database, working auth and email before you have provisioned anything at all.