.server
method.client
method.inputValidator
methodnext
Middleware allows you to customize the behavior of both server routes like GET/POST/etc (including requests to SSR your application) and server functions created with createServerFn. Middleware is composable and can even depend on other middleware to create a chain of operations that are executed hierarchically and in order.
There are two types of middleware: request middleware and server function middleware.
Note
Server function middleware is a subset of request middleware that has extra functionality specifically for server functions like being able to validate input data or perform client-side logic both before and after the server function is executed.
Feature | Request Middleware | Server Function Middleware |
---|---|---|
Scope | All server requests | Server functions only |
Methods | .server() | .client(), .server() |
Input Validation | No | Yes (.inputValidator()) |
Client-side Logic | No | Yes |
Dependencies | Can depend on request middleware | Can depend on both types |
Note
Request middleware cannot depend on server function middleware, but server function middleware can depend on request middleware.
All middleware is composable, which means that one middleware can depend on another middleware.
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(() => {
//...
})
const authMiddleware = createMiddleware()
.middleware([loggingMiddleware])
.server(() => {
//...
})
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(() => {
//...
})
const authMiddleware = createMiddleware()
.middleware([loggingMiddleware])
.server(() => {
//...
})
Middleware is next-able, which means that you must call the next function in the .server method (and/or .client method if you are creating a server function middleware) to execute the next middleware in the chain. This allows you to:
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
const result = await next() // <-- This will execute the next middleware in the chain
return result
})
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(async ({ next }) => {
const result = await next() // <-- This will execute the next middleware in the chain
return result
})
Request middleware is used to customize the behavior of any server request that passes through it, including both server routes and server functions.
To create a request middleware, call the createMiddleware function. You may call this function with the type property set to 'request', but this is the default value so you can omit it if you'd like.
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(() => {
//...
})
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(() => {
//...
})
Request middleware has the following methods:
The .server method is used to define server-side logic that the middleware will execute before any nested middleware, and also provide the result to the next middleware. It receives the next method and other things like context and the request object:
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(
({ next, context, request }) => {
return next()
},
)
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(
({ next, context, request }) => {
return next()
},
)
To quickly visualize this handshake, here is a diagram:
You can use request middleware with server routes in two ways:
To have a server route use middleware for all methods, pass a middleware array to the middleware property of the method builder object.
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(() => {
//...
})
export const Route = createFileRoute('/foo')({
server: {
middleware: [loggingMiddleware],
handlers: {
GET: () => {
//...
},
POST: () => {
//...
},
},
},
})
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware().server(() => {
//...
})
export const Route = createFileRoute('/foo')({
server: {
middleware: [loggingMiddleware],
handlers: {
GET: () => {
//...
},
POST: () => {
//...
},
},
},
})
You can pass middleware to specific server route methods by using the createHandlers utility and passing a middleware array to the middleware property of the method object.
const loggingMiddleware = createMiddleware().server(() => {
//...
})
export const Route = createFileRoute('/foo')({
server: {
handlers: ({ createHandlers }) =>
createHandlers({
GET: {
middleware: [loggingMiddleware],
handler: () => {
//...
},
},
}),
},
})
const loggingMiddleware = createMiddleware().server(() => {
//...
})
export const Route = createFileRoute('/foo')({
server: {
handlers: ({ createHandlers }) =>
createHandlers({
GET: {
middleware: [loggingMiddleware],
handler: () => {
//...
},
},
}),
},
})
Server function middleware is a subset of request middleware that has extra functionality specifically for server functions like being able to validate input data or perform client-side logic both before and after the server function is executed.
To create a server function middleware, call the createMiddleware function with the type property set to 'function'.
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware({ type: 'function' })
.client(() => {
//...
})
.server(() => {
//...
})
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware({ type: 'function' })
.client(() => {
//...
})
.server(() => {
//...
})
Server function middleware has the following methods:
Note
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 .client method is used to define client-side logic that the middleware will wrap the execution and result of the RPC call to the server.
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next, context, request }) => {
const result = await next() // <-- This will execute the next middleware in the chain and eventually, the RPC to the server
return result
},
)
import { createMiddleware } from '@tanstack/solid-start'
const loggingMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next, context, request }) => {
const result = await next() // <-- This will execute the next middleware in the chain and eventually, the RPC to the server
return result
},
)
The inputValidator 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.
import { createMiddleware } from '@tanstack/solid-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware({ type: 'function' })
.inputValidator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
import { createMiddleware } from '@tanstack/solid-start'
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const mySchema = z.object({
workspaceId: z.string(),
})
const workspaceMiddleware = createMiddleware({ type: 'function' })
.inputValidator(zodValidator(mySchema))
.server(({ next, data }) => {
console.log('Workspace ID:', data.workspaceId)
return next()
})
To have a middleware wrap a specific server function, you can pass a middleware array to the middleware property of the createServerFn function.
import { createServerFn } from '@tanstack/solid-start'
import { loggingMiddleware } from './middleware'
const fn = createServerFn()
.middleware([loggingMiddleware])
.handler(async () => {
//...
})
import { createServerFn } from '@tanstack/solid-start'
import { loggingMiddleware } from './middleware'
const fn = createServerFn()
.middleware([loggingMiddleware])
.handler(async () => {
//...
})
To quickly visualize this handshake, here is a diagram:
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.
import { createMiddleware } from '@tanstack/solid-start'
const awesomeMiddleware = createMiddleware({ type: 'function' }).server(
({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
},
)
const loggingMiddleware = createMiddleware({ type: 'function' })
.middleware([awesomeMiddleware])
.server(async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
})
import { createMiddleware } from '@tanstack/solid-start'
const awesomeMiddleware = createMiddleware({ type: 'function' }).server(
({ next }) => {
return next({
context: {
isAwesome: Math.random() > 0.5,
},
})
},
)
const loggingMiddleware = createMiddleware({ type: 'function' })
.middleware([awesomeMiddleware])
.server(async ({ next, context }) => {
console.log('Is awesome?', context.isAwesome)
return next()
})
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({ type: 'function' })
.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({ type: 'function' })
.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.
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const requestLogger = createMiddleware({ type: 'function' })
.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()
})
import { zodValidator } from '@tanstack/zod-adapter'
import { z } from 'zod'
const requestLogger = createMiddleware({ type: 'function' })
.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. The returned object of calling next in client contains the context sent from server to the client and is type-safe.
Warning
The return type of next in client can only be inferred from middleware known in the current middleware chain. Therefore the most accurate return type of next is in middleware at the end of the middleware chain
const serverTimer = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
},
)
const requestLogger = createMiddleware({ type: 'function' })
.middleware([serverTimer])
.client(async ({ next }) => {
const result = await next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
return result
})
const serverTimer = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
return next({
sendContext: {
// Send the current time to the client
timeFromServer: new Date(),
},
})
},
)
const requestLogger = createMiddleware({ type: 'function' })
.middleware([serverTimer])
.client(async ({ next }) => {
const result = await next()
// Woah! We have the time from the server!
console.log('Time from the server:', result.context.timeFromServer)
return result
})
Global middleware runs automatically for every request in your application. This is useful for functionality like authentication, logging, and monitoring that should apply to all requests.
To have a middleware run for every request handled by Start, you can create a middleware and return it as requestMiddleware in the createStart function in your src/start.ts file:
// src/start.ts
import { createStart } from '@tanstack/solid-start'
const myGlobalMiddleware = createMiddleware().server(() => {
//...
})
export const startInstance = createStart(() => {
return {
requestMiddleware: [myGlobalMiddleware],
}
})
// src/start.ts
import { createStart } from '@tanstack/solid-start'
const myGlobalMiddleware = createMiddleware().server(() => {
//...
})
export const startInstance = createStart(() => {
return {
requestMiddleware: [myGlobalMiddleware],
}
})
Note
Global request middleware runs before every request, including both server routes and server functions.
To have a middleware run for every server function in your application, you can create a middleware and return it to the createStart function as functionMiddleware in your src/start.ts file:
// src/start.ts
import { createStart } from '@tanstack/solid-start'
import { loggingMiddleware } from './middleware'
export const startInstance = createStart(() => {
return {
functionMiddleware: [loggingMiddleware],
}
})
// src/start.ts
import { createStart } from '@tanstack/solid-start'
import { loggingMiddleware } from './middleware'
export const startInstance = createStart(() => {
return {
functionMiddleware: [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.
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.
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user) // <-- Now this will be typed!
// ...
})
import { authMiddleware } from './authMiddleware'
const fn = createServerFn()
.middleware([authMiddleware])
.handler(async ({ context }) => {
console.log(context.user) // <-- Now this will be typed!
// ...
})
Middleware is executed dependency-first, starting with global middleware, followed by server function middleware. The following example will log in this order:
const globalMiddleware1 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware1')
return next()
},
)
const globalMiddleware2 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware2')
return next()
},
)
const a = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware({ type: 'function' })
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware({ type: 'function' })
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware({ type: 'function' })
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
const globalMiddleware1 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware1')
return next()
},
)
const globalMiddleware2 = createMiddleware({ type: 'function' }).server(
async ({ next }) => {
console.log('globalMiddleware2')
return next()
},
)
const a = createMiddleware({ type: 'function' }).server(async ({ next }) => {
console.log('a')
return next()
})
const b = createMiddleware({ type: 'function' })
.middleware([a])
.server(async ({ next }) => {
console.log('b')
return next()
})
const c = createMiddleware({ type: 'function' })
.middleware()
.server(async ({ next }) => {
console.log('c')
return next()
})
const d = createMiddleware({ type: 'function' })
.middleware([b, c])
.server(async () => {
console.log('d')
})
const fn = createServerFn()
.middleware([d])
.server(async () => {
console.log('fn')
})
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({ type: 'function' }).client(
async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
},
)
import { getToken } from 'my-auth-library'
const authMiddleware = createMiddleware({ type: 'function' }).client(
async ({ next }) => {
return next({
headers: {
Authorization: `Bearer ${getToken()}`,
},
})
},
)
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.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.