next-safe-action
Advanced

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

src/lib/middleware/logging.ts
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:

src/lib/safe-action.ts
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:

src/lib/middleware/auth-guard.ts
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:

PropertyTypeDescription
ctxobjectRequired shape of the context
metadataSchemaRequired shape of metadata
serverErrorstring | ErrorRequired 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

src/lib/middleware/check-ownership.ts
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:

src/app/actions.ts
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:

PropertyTypeDescription
ctxobjectRequired shape of the context
metadataSchemaRequired shape of metadata
serverErrorstring | ErrorRequired server error type
parsedInputunknownRequired shape of validated input
clientInputunknownRequired shape of raw client input
bindArgsParsedInputsreadonly unknown[]Required shape of validated bind args
bindArgsClientInputsreadonly 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

On this page