next-safe-action
Advanced

Output Validation

By default, the return type of your server code is inferred by TypeScript. But if you want runtime validation of the output, ensuring your server code returns exactly the expected shape, use outputSchema().

Why validate output?

  • Contract enforcement: Guarantee the shape of data sent to the client, even as server code evolves
  • Type safety from schema: The data type in the result comes from the output schema, not the function return type
  • Defense in depth: Catch bugs where server code accidentally returns extra or missing fields

Usage

Define an output schema

Chain .outputSchema() before .action():

src/app/actions.ts
"use server";

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

const outputSchema = z.object({
	id: z.string(),
	name: z.string(),
	email: z.string().email(),
});

export const getUser = actionClient
	.inputSchema(z.object({ userId: z.string() }))
	.outputSchema(outputSchema)
	.action(async ({ parsedInput }) => {
		const user = await db.user.findUnique({ where: { id: parsedInput.userId } });

		// TypeScript ensures this matches outputSchema
		return { id: user.id, name: user.name, email: user.email };
	});

Handle output validation errors

If your server code returns data that doesn't match the output schema, the action fails with a server error (not a validation error, since output issues are server-side bugs):

// If the server code returns { id: 123, name: "Alice" } (missing email),
// the action returns:
// { serverError: "Something went wrong" }

The actual ActionOutputDataValidationError is caught by handleServerError, so you can log it:

const actionClient = createSafeActionClient({
	handleServerError(e) {
		if (e.constructor.name === "ActionOutputDataValidationError") {
			console.error("Output validation failed:", e.message);
		}
		return "Something went wrong";
	},
});

Output validation is optional and most useful for API-like actions where the contract between server and client must be strict. For internal UI actions, TypeScript inference is usually sufficient.

See also

On this page