Problem
Middleware can provide context to downstream middleware/handlers with full type inference, but there's no way for a middleware to declare a required context. The only option today is to compose the concrete provider directly via .middleware([provider]).
interface DatabaseClient {
query<T>(sql: string, params?: unknown[]): Promise<Array<T>>
}
class RealDatabaseClient implements DatabaseClient {} // talks to Postgres
class FakeDatabaseClient implements DatabaseClient {} // in-memory, for tests
const dbMiddleware = createMiddleware({ type: 'function' })
.server(({ next }) => next({ context: { db: new RealDatabaseClient() } }))
// what I want to write: a middleware that consumes a DatabaseClient
// without baking in which implementation supplies it
const userServiceMiddleware = createMiddleware({ type: 'function' })
.server(({ next, context }) => {
context.db // ❌ TS error, fair enough, nothing guarantees it exists
return next({ context: { userService: new UserService(context.db) } })
})
The supported pattern is composition:
const userServiceMiddleware = createMiddleware({ type: 'function' })
.middleware([dbMiddleware])
.server(({ next, context }) =>
next({ context: { userService: new UserService(context.db) } }))
This works and is type safe, but the provider is now hard-wired into the consumer. UserService was written against the DatabaseClient interface, yet the chain pins it to RealDatabaseClient:
- I can't swap
db for a different implementation (FakeDatabaseClient in tests, transactional or per-tenant clients in requests). dbMiddleware always runs and always constructs the real one, so the interface may as well not exist.
- A server function listing
.middleware([userServiceMiddleware]) silently drags in dbMiddleware too, so you can't see an endpoint's full dependency graph where the endpoint is defined.
- There's no seam for testing, since there's no way to substitute what a middleware provides.
Proposal
Let a middleware declare a typed requirement — depend on the interface, let the chain supply the implementation:
const userServiceMiddleware = createMiddleware({ type: 'function' })
.requires<{ db: DatabaseClient }>() // 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(...))
// ✅ any provider satisfying the interface works — this is the DI seam
const fakeDbMiddleware = createMiddleware({ type: 'function' })
.server(({ next }) => next({ context: { db: new FakeDatabaseClient() } }))
const getUserForTest = createServerFn()
.middleware([fakeDbMiddleware, userServiceMiddleware])
.handler(({ context }) => context.userService.getById(...))
// ❌ compile error: userServiceMiddleware requires { db: DatabaseClient },
// nothing earlier in the chain provides it
const broken = createServerFn()
.middleware([userServiceMiddleware])
.handler(...)
.requires<T>() adds T to the middleware's server context type but contributes nothing at runtime. The check happens wherever chains are assembled (createServerFn().middleware([...]), createMiddleware().middleware([...]), global middleware in createStart): walk the flattened chain in order and make sure each middleware's requirements are covered by the accumulated context of earlier middleware.
It's purely additive. Existing composition keeps working, and requires is basically composition minus the hard-wired provider. The nice side effects: middleware stays small and single-purpose (an auth middleware can require { sessionService } without knowing how sessions are stored), and the .middleware([...]) array becomes a compiler-checked list of everything an endpoint actually depends on.
The testing angle
Server functions are where the business logic lives, but right now the only way to substitute a dependency in a test is module mocking:
vi.mock('~/server/user/service', () => ({
userService: { getById: vi.fn().mockResolvedValue(fakeUser) },
}))
vi.mock('~/server/email/service', () => ({
emailService: { send: vi.fn() },
}))
That works, but it couples tests to file layout, comes with hoisting gotchas, and nothing checks that the fake matches the real service. Rename a method and the mocks keep passing while production breaks.
With requires, a dependency is just a typed context slot, so a fake is just another value that satisfies it — and the compiler enforces that FakeDatabaseClient actually implements DatabaseClient, so fakes can't silently drift from the real thing:
import { testServerFn } from '@tanstack/react-start/testing' // hypothetical
const result = await testServerFn(getUser, {
data: { id: '123' },
// checked against the chain's declared requirements
context: { db: new FakeDatabaseClient(), userService: fakeUserService },
})
You could either run the real chain with some slots pre-seeded (in-memory db, fake email client), or skip middleware entirely and call the handler with a fully specified context. The runtime pieces mostly exist already (executeMiddleware and flattenMiddlewares are exported from @tanstack/start-client-core), but without a typed contract for what a chain needs, a public testing API can't really be sound. requires would provide that contract.
Happy to help with the API design if there's interest in this.
Problem
Middleware can provide context to downstream middleware/handlers with full type inference, but there's no way for a middleware to declare a required context. The only option today is to compose the concrete provider directly via
.middleware([provider]).The supported pattern is composition:
This works and is type safe, but the provider is now hard-wired into the consumer.
UserServicewas written against theDatabaseClientinterface, yet the chain pins it toRealDatabaseClient:dbfor a different implementation (FakeDatabaseClientin tests, transactional or per-tenant clients in requests).dbMiddlewarealways runs and always constructs the real one, so the interface may as well not exist..middleware([userServiceMiddleware])silently drags indbMiddlewaretoo, so you can't see an endpoint's full dependency graph where the endpoint is defined.Proposal
Let a middleware declare a typed requirement — depend on the interface, let the chain supply the implementation:
.requires<T>()addsTto the middleware's server context type but contributes nothing at runtime. The check happens wherever chains are assembled (createServerFn().middleware([...]),createMiddleware().middleware([...]), global middleware increateStart): walk the flattened chain in order and make sure each middleware's requirements are covered by the accumulated context of earlier middleware.It's purely additive. Existing composition keeps working, and
requiresis basically composition minus the hard-wired provider. The nice side effects: middleware stays small and single-purpose (an auth middleware can require{ sessionService }without knowing how sessions are stored), and the.middleware([...])array becomes a compiler-checked list of everything an endpoint actually depends on.The testing angle
Server functions are where the business logic lives, but right now the only way to substitute a dependency in a test is module mocking:
That works, but it couples tests to file layout, comes with hoisting gotchas, and nothing checks that the fake matches the real service. Rename a method and the mocks keep passing while production breaks.
With
requires, a dependency is just a typed context slot, so a fake is just another value that satisfies it — and the compiler enforces thatFakeDatabaseClientactually implementsDatabaseClient, so fakes can't silently drift from the real thing:You could either run the real chain with some slots pre-seeded (in-memory db, fake email client), or skip middleware entirely and call the handler with a fully specified context. The runtime pieces mostly exist already (
executeMiddlewareandflattenMiddlewaresare exported from@tanstack/start-client-core), but without a typed contract for what a chain needs, a public testing API can't really be sound.requireswould provide that contract.Happy to help with the API design if there's interest in this.