Middleware allows you to customize the behavior of server functions created with createServerFn with things like shared validation, context, and much more. Middleware can even depend on other middleware to create a chain of operations that are executed hierarchically and in order.
Middleware is defined using the createMiddleware function. This function returns a Middleware object that can be used to continue customizing the middleware with methods like middleware, validator, server, and client.
import { createMiddleware } from '@tanstack/start'
const loggingMiddleware = createMiddleware().server(async ({ next, data }) => {
console.log('Request received:', data)
const result = await next()
console.log('Response processed:', result)
return result
})
import { createMiddleware } from '@tanstack/start'
const loggingMiddleware = createMiddleware().server(async ({ next, data }) => {
console.log('Request received:', data)
const result = await next()
console.log('Response processed:', result)
return result
})
Several methods are available to customize the middleware. If you are (hopefully) using TypeScript, the order of these methods is enforced by the type system to ensure maximum inference and type safety.
The middleware method is used to dependency middleware to the chain that will executed before the current middleware. Just call the middleware method with an array of middleware objects.
const loggingMiddleware = createMiddleware().middleware([
authMiddleware,
loggingMiddleware,
])
const loggingMiddleware = createMiddleware().middleware([
authMiddleware,
loggingMiddleware,
])
Type-safe context and payload validation are also inherited from parent middlewares!
The validator method is used to modify the data object before it is passed to this middleware, nested middleware, and ultimately the server function. This method should receive a function that takes the data object and returns a validated (and optionally modified) data object. It's common to use a validation library like zod to do this. Here is an example:
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware()
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware()
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
The server method is used to define server-side logic that the middleware will execute both before and after any nested middleware and ultimately a server function. This method receives an object with the following properties:
The next function is used to execute the next middleware in the chain. You must await and return (or return directly) the result of the next function provided to you for the chain to continue executing.
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
console.log('Request received')
const result = await next()
console.log('Response processed')
return result
})
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
console.log('Request received')
const result = await next()
console.log('Response processed')
return result
})
The next function can be optionally called with an object that has a context property with an object value. Whatever properties you pass to this context value will be merged into the parent context and provided to the next middleware.
const awesomeMiddleware = createMiddleware().server(({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
})
const loggingMiddleware = createMiddleware().server(
async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
},
)
const awesomeMiddleware = createMiddleware().server(({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
})
const loggingMiddleware = createMiddleware().server(
async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
},
)
Despite server functions being mostly server-side bound operations, there is still plenty of client-side logic surrounding the outgoing RPC request from the client. This means that we can also define client-side logic in middleware that will execute on the client side around any nested middleware and ultimately the RPC function and its response to the client.
By default, middleware validation is only performed on the server to keep the client bundle size small. However, you may also choose to validate data on the client side by passing the validateClient: true option to the createMiddleware function. This will cause the data to be validated on the client side before being sent to the server, potentially saving a round trip.
Why can't I pass a different validation schema for the client?
The client-side validation schema is derived from the server-side schema. This is because the client-side validation schema is used to validate the data before it is sent to the server. If the client-side schema were different from the server-side schema, the server would receive data that it did not expect, which could lead to unexpected behavior.
const workspaceMiddleware = createMiddleware({ validateClient: true })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
const workspaceMiddleware = createMiddleware({ validateClient: true })
.validator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
Client middleware logic is defined using the client method on a Middleware object. This method is used to define client-side logic that the middleware will execute both before and after any nested middleware and ultimately the client-side RPC function (or the server-side function if you're doing SSR or calling this function from another server function).
Client-side middleware logic shares much of the same API as logic created with the server method, but it is executed on the client side. This includes:
Similar to the server function, it also receives an object with the following properties:
const loggingMiddleware = createMiddleware().client(async ({ next }) => {
console.log('Request sent')
const result = await next()
console.log('Response received')
return result
})
const loggingMiddleware = createMiddleware().client(async ({ next }) => {
console.log('Request sent')
const result = await next()
console.log('Response received')
return result
})
Client context is NOT sent to the server by default since this could end up unintentionally sending large payloads to the server. If you need to send client context to the server, you must call the next function with a sendContext property and object to transmit any data to the server. Any properties passed to sendContext will be merged, serialized and sent to the server along with the data and will be available on the normal context object of any nested server middleware.
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
// Send the workspace ID to the server
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Woah! We have the workspace ID from the client!
console.log('Workspace ID:', context.workspaceId)
return next()
})
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
// Send the workspace ID to the server
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Woah! We have the workspace ID from the client!
console.log('Workspace ID:', context.workspaceId)
return next()
})
You may have noticed that in the example above that while client-sent context is type-safe, it is is not required to be validated at runtime. If you pass dynamic user-generated data via context, that could pose a security concern, so if you are sending dynamic data from the client to the server via context, you should validate it in the server-side middleware before using it. Here's an example:
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Validate the workspace ID before using it
const workspaceId = zodValidator(z.number()).parse(context.workspaceId)
console.log('Workspace ID:', workspaceId)
return next()
})
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
return next({
sendContext: {
workspaceId: context.workspaceId,
},
})
})
.server(async ({ next, data, context }) => {
// Validate the workspace ID before using it
const workspaceId = zodValidator(z.number()).parse(context.workspaceId)
console.log('Workspace ID:', workspaceId)
return next()
})
Similar to sending client context to the server, you can also send server context to the client by calling the next function with a sendContext property and object to transmit any data to the client. Any properties passed to sendContext will be merged, serialized and sent to the client along with the response and will be available on the normal context object of any nested client middleware.
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
const result = next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
})
.server(async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
})
const requestLogger = createMiddleware()
.client(async ({ next, context }) => {
const result = next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
})
.server(async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
})
Middleware that uses the server method executes in the same context as server functions, so you can follow the exact same Server Function Context Utilities to read and modify anything about the request headers, status codes, etc.
Middleware that uses the client method executes in a completely different client-side context than server functions, so you can't use the same utilities to read and modify the request. However, you can still modify the request returning additional properties when calling the next function. Currently supported properties are:
Here's an example of adding an Authorization header any request using this middleware:
import { getToken } from 'my-auth-library'
const authMiddleware = createMiddleware().client(async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
})
import { getToken } from 'my-auth-library'
const authMiddleware = createMiddleware().client(async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
})
Middleware can be used in two different ways:
Global middleware is registered using the registerGlobalMiddleware function. This function receives an array of middleware to be appended to the global middleware array. There is currently no way to remove global middleware once it has been registered. If you need this functionality, please let us know by opening an issue on GitHub.
Here's an example of registering global middleware:
import { registerGlobalMiddleware } from '@tanstack/start'
registerGlobalMiddleware({
middleware: [authMiddleware, loggingMiddleware],
})
import { registerGlobalMiddleware } from '@tanstack/start'
registerGlobalMiddleware({
middleware: [authMiddleware, loggingMiddleware],
})
Global middleware types are inherently detached from server functions themselves. This means that if a global middleware supplies additional context to server functions or other server function specific middleware, the types will not be automatically passed through to the server function or other server function specific middleware.
// globalMiddleware.ts
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// serverFunction.ts
const authMiddleware = createMiddleware().server(({ next, context }) => {
console.log(context.user) // <-- This will not be typed!
// ...
})
// globalMiddleware.ts
registerGlobalMiddleware({
middleware: [authMiddleware],
})
// serverFunction.ts
const authMiddleware = createMiddleware().server(({ next, context }) => {
console.log(context.user) // <-- This will not be typed!
// ...
})
To solve this, add the global middleware you are trying to reference to the server function's middleware array. The global middleware will be deduped to a single entry (the global instance), and your server function will receive the correct types.
Here's an example of how this works:
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user)
// ...
})
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user)
// ...
})
Middleware is executed dependency-first, starting with global middleware, followed by server function middleware. The following example will log the following in this order:
const globalMiddleware1 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware1')
return next()
})
const globalMiddleware2 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware2')
return next()
})
registerGlobalMiddleware({
middleware: [globalMiddleware1, globalMiddleware2],
})
const a = createMiddleware().server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware()
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware()
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware()
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
const globalMiddleware1 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware1')
return next()
})
const globalMiddleware2 = createMiddleware().server(async ({ next }) => {
console.log('globalMiddleware2')
return next()
})
registerGlobalMiddleware({
middleware: [globalMiddleware1, globalMiddleware2],
})
const a = createMiddleware().server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware()
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware()
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware()
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
Middleware functionality is tree-shaken based on the environment for each bundle produced.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.