WIP: feat(http): typed context extension via next(extra) in middleware #14

Draft
codinget wants to merge 1 commits from worktree-bridge-cse_01XUhonMp8zrZxUpQHFuASV2 into main
Owner

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:

  1. The class gains a type parameter `T` (default `{}`) tracking the accumulated context type. Route methods accept `Handler<T & Match>` / `Middleware<T & Match>`.

  2. 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>`.

  3. 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)
```

## 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<TAdd>\`** (\`types.ts\`) — conditional generic: resolves to \`() => Promise<void>\` when \`TAdd\` is the empty object, and \`(extra: TAdd) => Promise<void>\` otherwise. \`Middleware<T>\` is unchanged (uses \`Next<{}>\` = \`() => Promise<void>\`), so all existing code keeps working. **\`Router<T>\`** (\`router.ts\`) — three coordinated changes: 1. The class gains a type parameter \`T\` (default \`{}\`) tracking the accumulated context type. Route methods accept \`Handler<T & Match>\` / \`Middleware<T & Match>\`. 2. Two context-extending \`use()\` overloads are placed *before* the existing non-extending ones. Because their \`next\` is typed as \`(extra: TAdd) => Promise<void>\` (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>\`. 3. 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<TAdd>\` (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) \`\`\`
codinget added 1 commit 2026-05-29 02:45:23 +02:00
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>
This pull request has changes conflicting with the target branch.
  • README.md
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin worktree-bridge-cse_01XUhonMp8zrZxUpQHFuASV2:worktree-bridge-cse_01XUhonMp8zrZxUpQHFuASV2
git checkout worktree-bridge-cse_01XUhonMp8zrZxUpQHFuASV2
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: webnet/webnet#14