Summary
Server function middleware can provide context to downstream middleware/handlers with full type inference, but it cannot declare a typed requirement on upstream context. The only way for a middleware to consume typed context today is to compose its provider directly via .middleware([provider]) — which hard-couples a concrete provider into every consumer.
Proposal: a requires capability — a middleware declares the context shape it needs, and TypeScript enforces at the createServerFn().middleware([...]) call site that an earlier middleware in the chain provides it.
The problem today
const dbMiddleware = createMiddleware({ type: 'function' })
.server(({ next }) => next({ context: { db } }))
// What I want to write: an atomic middleware that CONSUMES db without
// baking in WHICH provider supplies it
const userServiceMiddleware = createMiddleware({ type: 'function' })
.server(({ next, context }) => {
context.db // ❌ TS error — and rightly so, nothing guarantees it exists
return next({ context: { userService: new UserService(context.db) } })
})
The supported pattern is composition:
const userServiceMiddleware = createMiddleware({ type: 'function' })
.middleware([dbMiddleware]) // provider is now hard-wired into the consumer
.server(({ next, context }) =>
next({ context: { userService: new UserService(context.db) } }))
This works and is type-safe, but:
- No inversion of control. The consumer picks its concrete provider forever. You can't supply a different
db (per-tenant, transactional, or test) — the original provider always runs.
- Implicit dependencies. A server function listing
.middleware([userServiceMiddleware]) silently drags in dbMiddleware. The endpoint's full dependency graph isn't visible where the endpoint is defined.
- No test seam. Because providers are baked in, there's no supported way to execute a server function or middleware with substituted context.
Proposed API
const userServiceMiddleware = createMiddleware({ type: 'function' })
.requires<{ db: DB }>() // typed hole; provides nothing at runtime
.server(({ next, context }) => {
context.db // ✅ typed via the requirement
return next({ context: { userService: new UserService(context.db) } })
})
// ✅ requirement satisfied by an earlier middleware in the chain
const getUser = createServerFn()
.middleware([dbMiddleware, userServiceMiddleware])
.handler(({ context }) => context.userService.getById(...))
// ❌ compile error: userServiceMiddleware requires { db: DB },
// not provided by any earlier middleware in this chain
const broken = createServerFn()
.middleware([userServiceMiddleware])
.handler(...)
Semantics:
.requires<T>() adds T to the middleware's server context type without contributing runtime context.
- Validation happens wherever chains are assembled (
createServerFn().middleware([...]), createMiddleware().middleware([...]), createStart global middleware): walking the flattened chain in order, every middleware's requirements must be satisfied by the accumulated context of earlier middleware.
Benefits
- Atomic, single-purpose middleware.
dbMiddleware, userServiceMiddleware, isAuthed each do one thing. Auth middleware requires { sessionService } without knowing how sessions are stored.
- Explicit per-endpoint dependency manifests. The
.middleware([...]) array becomes a readable, compiler-verified list of everything an endpoint depends on.
- Inversion of control without a container. Providers are chosen at the edge, not baked into consumers — swapping in a tenant-scoped or transaction-scoped
db is a one-line, type-checked chain change.
- Incremental adoption. Purely additive; existing composition keeps working.
requires is just "composition minus the hard-wired provider".
How this unlocks a testing story
Server functions are where an app's business logic lives, but they're awkward to test today. Because middleware providers are hard-wired, the only way to substitute a dependency is module mocking:
// today: intercept the modules the implementation happens to import
vi.mock('~/server/user/service', () => ({
userService: { getById: vi.fn().mockResolvedValue(fakeUser) },
}))
vi.mock('~/server/email/service', () => ({
emailService: { send: vi.fn() },
}))
This works, but it's messy: string paths coupled to file layout, hoisting rules, and mock state leaking between tests. Worse, nothing checks that a fake matches the real service — rename a method and the mocks keep passing while production breaks.
With requires, a dependency is a typed context slot, and a fake is just another value that satisfies it. No module interception, and TypeScript keeps every fake in sync with the real interface:
import { testServerFn } from '@tanstack/react-start/testing' // hypothetical
const fakeUserService: UserService = {
getById: async () => fakeUser,
// ...
}
const result = await testServerFn(getUser, {
data: { id: '123' },
// checked against the chain's declared requirements — a missing slot won't compile
context: { db: pgliteDb, userService: fakeUserService },
})
Two testing modes fall out naturally:
- Provider substitution — run the real middleware chain, but pre-seed some
requires slots with fakes (in-memory db, fake email client, frozen clock) while the rest of the chain runs for real.
- Handler isolation — skip middleware entirely and invoke the handler with a fully-specified, type-checked context.
The runtime pieces mostly exist (executeMiddleware, flattenMiddlewares are exported from @tanstack/start-client-core), but there's no typed contract that would make a public testing API sound. requires provides that contract.
Happy to help iterate on the API design if there's interest.
Summary
Server function middleware can provide context to downstream middleware/handlers with full type inference, but it cannot declare a typed requirement on upstream context. The only way for a middleware to consume typed context today is to compose its provider directly via
.middleware([provider])— which hard-couples a concrete provider into every consumer.Proposal: a
requirescapability — a middleware declares the context shape it needs, and TypeScript enforces at thecreateServerFn().middleware([...])call site that an earlier middleware in the chain provides it.The problem today
The supported pattern is composition:
This works and is type-safe, but:
db(per-tenant, transactional, or test) — the original provider always runs..middleware([userServiceMiddleware])silently drags indbMiddleware. The endpoint's full dependency graph isn't visible where the endpoint is defined.Proposed API
Semantics:
.requires<T>()addsTto the middleware's server context type without contributing runtime context.createServerFn().middleware([...]),createMiddleware().middleware([...]),createStartglobal middleware): walking the flattened chain in order, every middleware's requirements must be satisfied by the accumulated context of earlier middleware.Benefits
dbMiddleware,userServiceMiddleware,isAuthedeach do one thing. Auth middleware requires{ sessionService }without knowing how sessions are stored..middleware([...])array becomes a readable, compiler-verified list of everything an endpoint depends on.dbis a one-line, type-checked chain change.requiresis just "composition minus the hard-wired provider".How this unlocks a testing story
Server functions are where an app's business logic lives, but they're awkward to test today. Because middleware providers are hard-wired, the only way to substitute a dependency is module mocking:
This works, but it's messy: string paths coupled to file layout, hoisting rules, and mock state leaking between tests. Worse, nothing checks that a fake matches the real service — rename a method and the mocks keep passing while production breaks.
With
requires, a dependency is a typed context slot, and a fake is just another value that satisfies it. No module interception, and TypeScript keeps every fake in sync with the real interface:Two testing modes fall out naturally:
requiresslots with fakes (in-memory db, fake email client, frozen clock) while the rest of the chain runs for real.The runtime pieces mostly exist (
executeMiddleware,flattenMiddlewaresare exported from@tanstack/start-client-core), but there's no typed contract that would make a public testing API sound.requiresprovides that contract.Happy to help iterate on the API design if there's interest.