Standalone middleware
The createMiddleware() helper lets you define middleware functions outside of a client, with proper type constraints. This is useful for sharing middleware across multiple clients or publishing middleware as a package.
Why standalone middleware?
When you define middleware inline with .use(), the types are inferred from the client chain. But if you want to define middleware in a separate file, or share it across clients, you need a way to declare the type requirements upfront. That's what createMiddleware() does.
Usage
import { createMiddleware } from "next-safe-action";
export const loggingMiddleware = createMiddleware().define(async ({ next, metadata }) => {
const start = Date.now();
const result = await next();
console.log(`Action took ${Date.now() - start}ms`, metadata);
return result;
});Then use it with any client:
import { createSafeActionClient } from "next-safe-action";
import { loggingMiddleware } from "./middleware/logging";
export const actionClient = createSafeActionClient().use(loggingMiddleware);Type constraints
If your middleware needs specific properties on the context, server error, or metadata, declare them as a generic:
import { createMiddleware } from "next-safe-action";
// This middleware requires ctx to have a `user` property
export const adminGuard = createMiddleware<{
ctx: { user: { role: string } };
}>().define(async ({ next, ctx }) => {
if (ctx.user.role !== "admin") {
throw new Error("Admin access required");
}
return next();
});Now TypeScript will error if you try to use adminGuard on a client that doesn't have user in its context:
// ✅ Works: authClient has ctx.user from auth middleware
const adminClient = authClient.use(adminGuard);
// ❌ Type error: actionClient doesn't have ctx.user
const broken = actionClient.use(adminGuard);Available constraints
The generic parameter accepts these optional properties:
| Property | Type | Description |
|---|---|---|
ctx | object | Required shape of the context |
metadata | Schema | Required shape of metadata |
serverError | string | Error | Required server error type |
createMiddleware<{
ctx: { user: { id: string; role: string } };
metadata: { actionName: string };
}>().define(async ({ next, ctx, metadata }) => {
// ctx.user and metadata.actionName are typed
return next();
});The constraint defines the minimum required shape. The actual context/metadata can have additional properties, the middleware just needs these to be present.
Standalone validated middleware
The createValidatedMiddleware() helper works like createMiddleware(), but for middleware that runs after input validation via useValidated(). It lets you declare type constraints for parsedInput, clientInput, and bind args in addition to ctx and metadata.
Why standalone validated middleware?
Just as createMiddleware() lets you share pre-validation middleware across clients, createValidatedMiddleware() lets you share post-validation middleware. This is especially useful for authorization patterns that depend on input shape.
Usage
import { createValidatedMiddleware } from "next-safe-action";
export const checkOwnership = createValidatedMiddleware<{
ctx: { user: { id: string } };
parsedInput: { resourceId: string };
}>().define(async ({ parsedInput, ctx, next }) => {
const resource = await db.resource.findUnique({
where: { id: parsedInput.resourceId },
});
if (resource?.ownerId !== ctx.user.id) {
throw new Error("Not authorized");
}
return next({ ctx: { resource } });
});Then use it with any client that has the required context and schema:
import { checkOwnership } from "@/lib/middleware/check-ownership";
export const updateResource = authClient
.inputSchema(z.object({ resourceId: z.string(), title: z.string() }))
.useValidated(checkOwnership)
.action(async ({ ctx }) => {
// ctx.resource is typed and available
});Available constraints
The generic parameter accepts all properties from createMiddleware() plus:
| Property | Type | Description |
|---|---|---|
ctx | object | Required shape of the context |
metadata | Schema | Required shape of metadata |
serverError | string | Error | Required server error type |
parsedInput | unknown | Required shape of validated input |
clientInput | unknown | Required shape of raw client input |
bindArgsParsedInputs | readonly unknown[] | Required shape of validated bind args |
bindArgsClientInputs | readonly unknown[] | Required shape of raw bind args |
The constraint defines the minimum required shape. The actual input can have additional properties: the middleware just needs these to be present.
See also
- Middleware guide: foundational middleware concepts and patterns
createMiddleware()API: full API referencecreateValidatedMiddleware()API: full API reference for validated standalone middleware- Validated middleware guide: when and how to use
useValidated() - Metadata: use metadata constraints in standalone middleware