WIP: feat(http): typed context extension via next(extra) in middleware #14
Reference in New Issue
Block a user
Delete Branch "worktree-bridge-cse_01XUhonMp8zrZxUpQHFuASV2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Adds the ability to extend the request context from middleware by passing an object to `next()`. The extensions are automatically typed for all downstream middlewares and handlers — no explicit casts needed.
```typescript
const app = new Server()
app
.use(async (ctx, next) => {
const user = await authenticate(ctx.req)
await next({ user }) // TypeScript infers TAdd = { user: User }
}) // returns Router<{ user: User }>
.get("/profile", async (ctx) => {
ctx.user // typed as User — no cast required
})
await app.listen(listener)
```
How
`Next` (`types.ts`) — conditional generic: resolves to `() => Promise` when `TAdd` is the empty object, and `(extra: TAdd) => Promise` otherwise. `Middleware` is unchanged (uses `Next<{}>` = `() => Promise`), so all existing code keeps working.
`Router` (`router.ts`) — three coordinated changes:
The class gains a type parameter `T` (default `{}`) tracking the accumulated context type. Route methods accept `Handler<T & Match>` / `Middleware<T & Match>`.
Two context-extending `use()` overloads are placed before the existing non-extending ones. Because their `next` is typed as `(extra: TAdd) => Promise` (required argument), TypeScript naturally fails them when `next()` is called without args and falls through to the `this`-returning overloads. When `next({ … })` is used, `TAdd` is inferred from the argument and the return type becomes `Router<T & TAdd>`.
The `handler` getter's chain builder was reworked: context is now threaded through the chain as an argument. `next(extra)` merges the extension into the running context object, so all downstream steps see the accumulated properties.
Fluent usage pattern — `use()` on a `Server` inherits from `Router<{}>` and returns `Router` (not `Server`). Since the chain mutates the same underlying object, routes added to the returned typed router are served by the original server:
```typescript
const app = new Server()
app.use(authMw).use(tenantMw).get("/", handler) // all on same object
await app.listen(listener)
```
Router<T> is now generic. Middlewares can call next({ key: value }) to extend the context for all downstream middlewares and handlers. TypeScript infers the added type from the argument — no casts or explicit annotations required by callers. The overload design places extending overloads (required next arg) before non-extending ones so TypeScript naturally falls through based on whether next() is called with or without arguments. The chain builder was reworked to thread context as an argument so extensions accumulate per-request. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>View command line instructions
Checkout
From your project repository, check out a new branch and test the changes.