mcpField
S
safe-action-client

safe-action-client

github.com/next-safe-actionDevelopment11.2k installsUpdated 8w ago
Repository

About safe-action-client

safe-action-client is a skill for defining and consuming next-safe-action server actions in TypeScript. It covers the chainable client API, schema validation, middleware, and client hooks.

Key features

  1. 01Configure chainable action clients
  2. 02Validate inputs and outputs with schemas
  3. 03Add pre- and post-validation middleware
  4. 04Define metadata and bound-argument schemas
  5. 05Use action and stateful client hooks

Use cases

  1. 01Set up typed server actions
  2. 02Validate action inputs and returned values
  3. 03Handle client-side action state with hooks

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/next-safe-action/skills --skill safe-action-client

After installing, type/safe-action-clientin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

client-setup.md
error-handling.md
input-output-validation.md

Details

From the project README

next-safe-action Client & Action Definition

Quick Start

// src/lib/safe-action.ts
import { createSafeActionClient } from "next-safe-action";

export const actionClient = createSafeActionClient();
// src/app/actions.ts
"use server";

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

export const greetUser = actionClient
  .inputSchema(z.object({ name: z.string().min(1) }))
  .action(async ({ parsedInput: { name } }) => {
    return { greeting: `Hello, ${name}!` };
  });

Chainable API Order

createSafeActionClient(opts?)
  .use(middleware)              // repeatable, adds pre-validation middleware
  .metadata(data)              // required if defineMetadataSchema is set
  .inputSchema(schema, utils?) // Standard Schema or async factory function
  .bindArgsSchemas([...])      // schemas for .bind() arguments (order with inputSchema is flexible)
  .useValidated(middleware)    // repeatable, adds post-validation middleware (requires inputSchema or bindArgsSchemas before it)
  .outputSchema(schema)        // validates action return value
  .action(serverCodeFn, utils?)      // creates SafeActionFn
  .stateAction(serverCodeFn, utils?) // creates SafeStateActionFn (for useStateAction, useOptimisticStateAction, or React's useActionState)

Each method returns a new client instance — the chain is immutable.

Entry Points

Entry pointEnvironmentExports
next-safe-actionServercreateSafeActionClient, createMiddleware, createValidatedMiddleware, returnValidationErrors, returnServerError, flattenValidationErrors, formatValidationErrors, DEFAULT_SERVER_ERROR_MESSAGE, error classes, all core types
next-safe-action/hooksClientuseAction, useOptimisticAction, useStateAction, useOptimisticStateAction, hook types
next-safe-action/stateful-hooksClientuseStateAction (re-export from hooks for backward compatibility)

Supporting Docs

Anti-Patterns

// BAD: Missing "use server" directive — action won't work
import { actionClient } from "@/lib/safe-action";
export const myAction = actionClient.action(async () => {});

// GOOD: Always include "use server" in action files
"use server";
import { actionClient } from "@/lib/safe-action";
export const myAction = actionClient.action(async () => {});
// BAD: Calling .action() without .metadata() when metadataSchema is defined
const client = createSafeActionClient({
  defineMetadataSchema: () => z.object({ actionName: z.string() }),
});
client.action(async () => {}); // TypeScript error!

// GOOD: Always provide metadata before .action() when schema is defined
client
  .metadata({ actionName: "myAction" })
  .action(async () => {});
// BAD: Returning an error instead of throwing
export const myAction = actionClient
  .inputSchema(z.object({ email: z.string().email() }))
  .action(async ({ parsedInput }) => {
    const exists = await db.user.findByEmail(parsedInput.email);
    if (exists) {
      return { error: "Email taken" }; // Not type-safe, not standardized
    }
  });

// GOOD: Use returnValidationErrors for field-level errors
import { returnValidationErrors } from "next-safe-action";

export const myAction = actionClient
  .inputSchema(z.object({ email: z.string().email() }))
  .action(async ({ parsedInput }) => {
    const exists = await db.user.findByEmail(parsedInput.email);
    if (exists) {
      returnValidationErrors(z.object({ email: z.string().email() }), {
        email: { _errors: ["Email is already in use"] },
      });
    }
    return { success: true };
  });

For expected non-validation business errors ("out of stock", "not found"), use returnServerError(payload) instead — it sets result.serverError to the typed payload, bypassing handleServerError. See Server error handling.

Server Code Function Parameters

The function passed to .action() receives a single object:

.action(async ({
  parsedInput,           // validated input (typed from inputSchema)
  clientInput,           // raw client input (unknown)
  bindArgsParsedInputs,  // validated bind args tuple
  bindArgsClientInputs,  // raw bind args
  ctx,                   // context from middleware chain
  metadata,              // metadata set via .metadata()
}) => {
  // return data
});

For .stateAction(), a second argument is added:

.stateAction(async ({ parsedInput, ctx }, { prevResult }) => {
  // prevResult is the previous SafeActionResult (structuredClone'd)
  return { count: (prevResult.data?.count ?? 0) + 1 };
});

Comments

Markdown supported