Skip to content
Back to skills

Next Action Handler

ASecurity

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.

  • 8 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added August 30, 2026
ai-agentsgobashapidatabase

Works with

  • cli
  • api

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 2 files and shows the line behind each finding

Scanned August 30, 2026

npx -y skills add mohamed-hossam1/nextjs-skills --skill next-action-handler --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Next Action Handler?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Next Action Handler
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/mohamed-hossam1-next-action-handler/badge)](https://www.skillsdirectory.com/skills/mohamed-hossam1-next-action-handler)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: next-action-handler
description: "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."
metadata:
  author: mohamed-hossam1
  version: 1.0.1
---

# 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.

```bash
npm install -D next-action-handler
```

## Usage

From the project root, run the installer:

```bash
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`.

```ts
// 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.

```ts
// 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.

```ts
// 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`.

```ts
// 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.

```ts
// 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.

| Class                 | Code                    | Expose | Default message             |
| --------------------- | ----------------------- | ------ | --------------------------- |
| `BadRequestError`     | `BAD_REQUEST`           | Yes    | "Bad request"               |
| `ValidationError`     | `VALIDATION_ERROR`      | Yes    | "Invalid input"             |
| `UnauthorizedError`   | `UNAUTHORIZED`          | Yes    | "Unauthorized"              |
| `ForbiddenError`      | `FORBIDDEN`             | Yes    | "Forbidden"                 |
| `NotFoundError`       | `NOT_FOUND`             | Yes    | "Resource not found"        |
| `RateLimitError`      | `RATE_LIMITED`          | Yes    | "Too many requests"         |
| `DatabaseError`       | `DATABASE_ERROR`        | No     | "Database operation failed" |
| `InternalServerError` | `INTERNAL_SERVER_ERROR` | No     | "Something went wrong"      |

## Error Converters

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

```ts
// 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](references/patterns.md)

Files in this skill

  • SKILL.md7.2 KB
  • references / patterns.md6.4 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.