next-safe-action
Advanced

Extend Schemas

The .inputSchema() method can accept an async function instead of a direct schema. This function receives the previous schema (if any) as an argument, letting you build on top of it. This is useful for creating action templates that share a base schema.

Basic usage

Pass an async function to inputSchema() that returns a new schema:

src/lib/safe-action.ts
import { z } from "zod";
import { actionClient } from "@/lib/safe-action";

// Template: all CRUD actions need an ID
const crudClient = actionClient
	.inputSchema(z.object({ id: z.string().uuid() }));

// Extend: update action adds name and email
export const updateUser = crudClient
	.inputSchema(async (prevSchema) => {
		// prevSchema is z.object({ id: z.string().uuid() })
		return prevSchema.extend({
			name: z.string().min(2),
			email: z.string().email(),
		});
	})
	.action(async ({ parsedInput }) => {
		// parsedInput is typed as { id: string, name: string, email: string }
	});

Multiple extensions

You can chain multiple inputSchema() calls, each building on the last:

const baseAction = actionClient
	.inputSchema(z.object({ orgId: z.string() }));

const teamAction = baseAction
	.inputSchema(async (prevSchema) => {
		return prevSchema.extend({ teamId: z.string() });
	});

const memberAction = teamAction
	.inputSchema(async (prevSchema) => {
		return prevSchema.extend({ memberId: z.string() });
	})
	.action(async ({ parsedInput }) => {
		// parsedInput: { orgId: string, teamId: string, memberId: string }
	});

The async function signature is key, as it lets you await other operations before returning the schema. This is useful for i18n where you need to load translations before building the schema.

Use cases

  • CRUD templates: Define a base schema with an ID field, then extend for create/update
  • Multi-tenant apps: Start with orgId, extend with resource-specific fields
  • i18n validation: Await translations before defining error messages in schemas

See also

On this page