Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
mohamed-hossam1 avatar

Next Action Handler

  • 3 installs
  • 8 repo stars
  • Updated May 30, 2026
  • mohamed-hossam1/nextjs-skills

Install and use a Next.js server-action layer built on next-safe-action, better-auth, pino, and zod with standardized errors, logging, and auth.

About

Sets up and uses next-action-handler, a server-action layer that standardizes errors, logging, and auth context via next-safe-action, better-auth, pino, and zod. A developer uses it to scaffold action clients, required actionName metadata, and authed actions in a Next.js app.

  • npx installer wires safe-action.ts, error classes, and pino logging
  • authedActionClient requires a requireUser auth-helper and actionName metadata

Next Action Handler by the numbers

  • 3 all-time installs (skills.sh)
  • Ranked #3,739 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
  • Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mohamed-hossam1/nextjs-skills --skill next-action-handler

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs3
repo stars8
Last updatedMay 30, 2026
Repositorymohamed-hossam1/nextjs-skills

What it does

Install and use a Next.js server-action layer built on next-safe-action, better-auth, pino, and zod with standardized errors, logging, and auth.

Files

SKILL.mdMarkdownGitHub ↗

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

  • Copy-paste patterns: references/patterns.md

Related skills

Backend & APIsbackendintegrations

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.