Middleware
Middleware lets you run code before and after your action's server code. Common uses include authentication, logging, rate limiting, and enriching the context with shared data.
How middleware executes
Middleware functions wrap your server code in layers. Each layer calls next() to invoke the next layer, and the result flows back outward:
Building middleware step by step
Add logging
The simplest middleware: log when actions start and finish:
import { createSafeActionClient } from "next-safe-action";
export const actionClient = createSafeActionClient().use(async ({ next, metadata }) => {
const start = Date.now();
const result = await next();
console.log(`Action ${JSON.stringify(metadata)} took ${Date.now() - start}ms`);
return result;
});The key pattern: do something before next(), await the result, do something after.
Add authentication
Check the session and pass the user into the context:
export const authClient = actionClient.use(async ({ next }) => {
const session = await getSession();
if (!session?.user) {
throw new Error("Not authenticated");
}
// Pass user data to the next layer via ctx
return next({ ctx: { user: session.user } });
});When you pass { ctx: { user } } to next(), it merges with the existing context. The next middleware (or your server code) can access ctx.user.
Use the context in actions
Now actions defined with authClient have typed access to ctx.user:
"use server";
import { authClient } from "@/lib/safe-action";
import { z } from "zod";
export const updateProfile = authClient
.inputSchema(z.object({ name: z.string().min(2) }))
.action(async ({ parsedInput, ctx }) => {
// ctx.user is typed — TypeScript knows it exists
await db.user.update({
where: { id: ctx.user.id },
data: { name: parsedInput.name },
});
return { success: true };
});Instance-level vs action-level middleware
Middleware added with .use() applies to different scopes depending on where you add it:
Instance-level (shared across actions)
// All actions using authClient will run this middleware
const authClient = actionClient.use(async ({ next }) => {
const session = await getSession();
if (!session) throw new Error("Unauthorized");
return next({ ctx: { user: session.user } });
});Action-level (specific to one action)
// Only this action runs the rate limit middleware
export const sensitiveAction = authClient
.use(async ({ next, ctx }) => {
await checkRateLimit(ctx.user.id);
return next();
})
.inputSchema(schema)
.action(async ({ parsedInput, ctx }) => {
// ...
});Middleware function arguments
Each middleware function receives a single object with these properties:
| Property | Type | Description |
|---|---|---|
clientInput | unknown | The raw, unvalidated input from the client |
bindArgsClientInputs | unknown[] | Raw bound argument values |
ctx | Ctx | Accumulated context from previous middleware |
metadata | MD | Action metadata (if defineMetadataSchema is configured) |
next | (opts?) => Promise<MiddlewareResult> | Call the next middleware or server code |
The next() function accepts an optional { ctx: { ... } } to extend the context for downstream layers.
Common patterns
Combining before/after logic
.use(async ({ next, metadata }) => {
// BEFORE: runs before server code
console.log("Starting:", metadata);
const startTime = performance.now();
const result = await next();
// AFTER: runs after server code (even if it failed)
const duration = performance.now() - startTime;
console.log(`Finished: ${metadata} in ${duration}ms`);
return result;
})Short-circuiting
Throwing an error in middleware stops the entire pipeline:
.use(async ({ next, ctx }) => {
if (ctx.user.isBanned) {
throw new Error("Account suspended");
// Server code never runs
}
return next();
})Passing data through context
Each middleware can add to the context, building up a rich object:
const actionClient = createSafeActionClient()
.use(async ({ next }) => {
return next({ ctx: { requestId: crypto.randomUUID() } });
})
.use(async ({ next }) => {
const session = await getSession();
return next({ ctx: { user: session?.user ?? null } });
});
// In your action: ctx = { requestId: string, user: User | null }