Guide
Middleware
Chain composable middleware for authentication, authorization, logging, and rate limiting with fully typed context and H3Event access.
Middleware
Middleware lets you run code before your action handler executes. Use it for authentication, authorization, logging, rate limiting, and more.
Basic Middleware
Add middleware to a client with .use():
actionClient.use(async ({ ctx, next, event, metadata, clientInput }) => {
// Run code before the action
console.log('Action called at:', new Date())
// Must call next() to continue the chain
return next({ ctx })
})
Middleware Parameters
| Parameter | Type | Description |
|---|---|---|
ctx | object | Context from previous middleware in the chain |
next | function | Call to continue to the next middleware or action handler |
event | H3Event | The H3 request event with full request access |
metadata | object | Metadata attached via .metadata() |
clientInput | unknown | Raw input before Zod validation |
Middleware must always call
next(). If it doesn't, the action will throw an error.Authentication Middleware
The most common use case — ensure the user is authenticated:
server/utils/action-client.ts
export const authActionClient = actionClient
.use(async ({ next, event }) => {
const session = await getUserSession(event)
if (!session) {
throw new Error('Unauthorized')
}
return next({ ctx: { userId: session.user.id, email: session.user.email } })
})
The ctx object is now typed with userId and email in all downstream middleware and the action handler.
Chaining Middleware
Middleware composes — each .use() adds to the chain:
export const adminActionClient = actionClient
// First: check authentication
.use(async ({ next, event }) => {
const session = await getUserSession(event)
if (!session) throw new Error('Unauthorized')
return next({ ctx: { userId: session.user.id } })
})
// Second: check admin role
.use(async ({ next, ctx }) => {
const user = await db.user.findUnique({ where: { id: ctx.userId } })
if (user?.role !== 'admin') throw new Error('Forbidden')
return next({ ctx: { ...ctx, isAdmin: true } })
})
Logging Middleware
actionClient.use(async ({ next, ctx, metadata }) => {
const start = Date.now()
const result = await next({ ctx })
const duration = Date.now() - start
console.log(`Action completed in ${duration}ms`)
return result
})
Metadata-based Middleware
Use .metadata() on actions and read it in middleware:
// Client with role-checking middleware
export const rbacClient = actionClient
.use(async ({ next, event, metadata }) => {
const session = await getUserSession(event)
if (!session) throw new Error('Unauthorized')
if (metadata.requiredRole) {
const user = await db.user.findUnique({ where: { id: session.user.id } })
if (user?.role !== metadata.requiredRole) {
throw new Error('Forbidden')
}
}
return next({ ctx: { userId: session.user.id } })
})
server/actions/delete-user.ts
export default rbacClient
.metadata({ requiredRole: 'admin' })
.schema(z.object({ userId: z.string() }))
.action(async ({ parsedInput, ctx }) => {
await db.user.delete({ where: { id: parsedInput.userId } })
return { deleted: true }
})
H3Event Access
The event parameter gives you full access to the H3 request context:
actionClient.use(async ({ next, event }) => {
// Read headers
const userAgent = getHeader(event, 'user-agent')
// Read cookies
const token = getCookie(event, 'auth-token')
// Access request info
const ip = getRequestIP(event)
return next({ ctx: { userAgent, ip } })
})