next-action-handler

v2026.09.24

Use when setting up or using next-action-handler, next-safe-action, actionClient/authedActionClient, action metadata/actionName, validationErrors, outputSchema, handleServerError, better-auth errors, or pino logging.

GitHub
安装命令
npx skhub add mohamed-hossam1/next-action-handler
Markdown
SKILL.md

next-action-handler Skill

What It Does

next-action-handler installs a server action layer built on next-safe-action, better-auth, pino, and zod. It standardizes errors, logging, and auth context.

Installation

If you want a local dev dependency, install it first. Otherwise skip to Usage.

npm install -D next-action-handler

Usage

From the project root, run the installer:

npx next-action-handler@latest add

@latest forces npx to use the newest published version. The installer applies the full handler setup in one pass, with no component selection.

Install path detection order:

  1. lib/ -> lib/next-action-handler/
  2. app/lib/ -> app/lib/next-action-handler/
  3. Otherwise create lib/next-action-handler/

Dependencies installed: better-auth, next-safe-action, pino, pino-pretty, server-only, zod.

Required: auth-helpers.ts

safe-action.ts imports requireUser from ../auth-helpers. Create it before using authedActionClient.

// Good: required for authedActionClient
import { headers } from "next/headers";
import { auth } from "./auth";
import { UnauthorizedError } from "./next-action-handler/error/errors";

export async function requireUser() {
  const session = await auth.api.getSession({ headers: await headers() });
  if (!session?.user) throw new UnauthorizedError("You must be logged in");
  return session.user;
}

Action Clients and Metadata

Every action must call .metadata({ actionName }). The metadata schema requires it and the logger uses it.

// Good: metadata actionName is required
import { z } from "zod";
import {
  actionClient,
  authedActionClient,
} from "@/lib/next-action-handler/safe-action";
import { db } from "@/lib/db";

export const submitContactForm = actionClient
  .metadata({ actionName: "submitContactForm" })
  .inputSchema(
    z.object({ email: z.string().email(), message: z.string().min(1) }),
  )
  .action(async ({ parsedInput }) => {
    return { success: true, email: parsedInput.email };
  });

export const updateProfile = authedActionClient
  .metadata({ actionName: "updateProfile" })
  .inputSchema(z.object({ displayName: z.string().min(1) }))
  .action(async ({ parsedInput, ctx }) => {
    await db.users.update({
      id: ctx.user.id,
      displayName: parsedInput.displayName,
    });
    return { updated: true };
  });

Result Shape and Input Validation

actionClient returns a SafeActionResult union. Only one of data, serverError, or validationErrors is present.

// Good: result union shape
type SafeActionResult<ServerError, Schema, ShapedErrors, Data> =
  | { data: Data; serverError?: undefined; validationErrors?: undefined }
  | { data?: undefined; serverError: ServerError; validationErrors?: undefined }
  | {
      data?: undefined;
      serverError?: undefined;
      validationErrors: ShapedErrors;
    };

Input schema failures land in validationErrors, not serverError.

// Good: check validationErrors before serverError
import { z } from "zod";
import { actionClient } from "@/lib/next-action-handler/safe-action";

const schema = z.object({
  email: z.string().email(),
  password: z.string().min(8, "Password must contain at least 8 characters"),
});

export const loginAction = actionClient
  .metadata({ actionName: "loginAction" })
  .inputSchema(schema)
  .action(async ({ parsedInput }) => {
    return { success: true, email: parsedInput.email };
  });

export async function submitLogin() {
  const result = await loginAction({ email: "bad", password: "short" });

  if (result.validationErrors) {
    console.error(result.validationErrors.email?._errors?.[0]);
    return;
  }
  if (result.serverError) {
    console.error(result.serverError.message);
    return;
  }

  console.log(result.data.email);
}

Output Validation

outputSchema mismatches become serverError (not validationErrors). Output validation failures are detected as ActionOutputDataValidationError and mapped to a PublicServerError with code INTERNAL_SERVER_ERROR and message Unexpected response. Please try again. The error is normalized and logged using InternalServerError("Action output validation failed", error).

handleServerError returns a PublicServerError shape { code, message } and uses DEFAULT_SERVER_ERROR_MESSAGE when expose is false for all other errors.

// Good: output validation returns serverError
import { z } from "zod";
import { actionClient } from "@/lib/next-action-handler/safe-action";
import { db } from "@/lib/db";

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

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

If you customize handleServerError, keep the ActionOutputDataValidationError branch from safe-action.ts. See patterns for a copy-paste block.

Error Classes

Throw these inside actions. They are normalized and logged; only safe messages reach the client.

ClassCodeExposeDefault message
BadRequestErrorBAD_REQUESTYes"Bad request"
ValidationErrorVALIDATION_ERRORYes"Invalid input"
UnauthorizedErrorUNAUTHORIZEDYes"Unauthorized"
ForbiddenErrorFORBIDDENYes"Forbidden"
NotFoundErrorNOT_FOUNDYes"Resource not found"
RateLimitErrorRATE_LIMITEDYes"Too many requests"
DatabaseErrorDATABASE_ERRORNo"Database operation failed"
InternalServerErrorINTERNAL_SERVER_ERRORNo"Something went wrong"

Error Converters

Use helpers when calling better-auth or database APIs that throw.

// Good: convert external errors into ActionError subclasses
import { fromBetterAuthError } from "@/lib/next-action-handler/error/better-auth-error";
import { toDatabaseError } from "@/lib/next-action-handler/error/database-error";

export function toAuthError(error: unknown) {
  return fromBetterAuthError(error, {
    enumerationSafe: true,
    genericMessage: "Invalid credentials",
  });
}

export function toDbError(error: unknown) {
  return toDatabaseError(error, "Database operation failed");
}

Logging

Logging is automatic. logActionExecution runs only when there is no serverError. logActionError runs for all errors.

References

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

2026年9月24日

分类

未分类

许可证

MIT

源路径

skills/next-action-handler

默认分支

main

最新提交

80d20aa

Tree SHA

4090dc9