home / skills / gilbertopsantosjr / fullstacknextjs / nextjs-server-actions

nextjs-server-actions skill

/skills/nextjs-server-actions

This skill guides implementing Next.js server actions with ZSA, including authentication, validation, and React Query integration for robust server-side

npx playbooks add skill gilbertopsantosjr/fullstacknextjs --skill nextjs-server-actions

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
3.6 KB
---
name: nextjs-server-actions
description: Guide for implementing Next.js server actions using ZSA (Zod Server Actions) with authentication, validation, and React Query integration. Use when creating API endpoints, form handlers, mutations, or any server-side data operations.
---

# Next.js Server Actions with ZSA

## File Structure

```
features/<feature>/usecases/
├── create/actions/create-<entity>-action.ts
├── update/actions/update-<entity>-action.ts
├── delete/actions/delete-<entity>-action.ts
└── list/actions/list-<entity>-action.ts
```

## Action Pattern

```typescript
// create-account-action.ts
'use server'

import 'server-only'
import { revalidatePath } from 'next/cache'
import { authedProcedure } from '@saas4dev/auth'
import { CreateAccountSchema, AccountSchema } from '@/features/accounts/model/account-schemas'
import { AccountService } from '@/features/accounts/account-service'

export const createAccountAction = authedProcedure
  .createServerAction()
  .input(CreateAccountSchema, { type: 'formData' })
  .output(AccountSchema)
  .onComplete(async () => {
    revalidatePath('/accounts')
  })
  .handler(async ({ input, ctx }) => {
    const result = await AccountService.create(ctx.userId, input)
    if (!result.success) {
      throw new Error(result.error)
    }
    return result.data
  })
```

## Required Directives

Every action file MUST include:
```typescript
'use server'           // First line - marks as server action
import 'server-only'   // Prevents client import
```

## Authentication

Use `authedProcedure` for protected actions:
```typescript
import { authedProcedure } from '@saas4dev/auth'

export const myAction = authedProcedure
  .createServerAction()
  .handler(async ({ ctx }) => {
    const userId = ctx.userId  // Available from auth context
  })
```

For public actions (no auth):
```typescript
import { createServerAction } from 'zsa'

export const publicAction = createServerAction()
  .input(schema)
  .handler(async ({ input }) => { ... })
```

## Client Integration with React Query

```typescript
'use client'
import { useServerActionMutation, useServerActionQuery } from '@saas4dev/core'

// Mutations (create, update, delete)
const mutation = useServerActionMutation(createAccountAction, {
  onSuccess: () => toast.success('Created'),
  onError: (error) => toast.error(error.message),
})

mutation.mutate(formData)

// Queries (read, list)
const { data, isLoading } = useServerActionQuery(listAccountsAction, {
  input: { userId },
})
```

## Form Submission

```typescript
'use client'
import { useForm } from 'react-hook-form'
import { useServerActionMutation } from '@saas4dev/core'

export function CreateForm() {
  const form = useForm<Input>({ resolver: zodResolver(Schema) })
  const mutation = useServerActionMutation(createAction)

  const onSubmit = (data: Input) => mutation.mutate(data)

  return (
    <form onSubmit={form.handleSubmit(onSubmit)}>
      {/* fields */}
      <Button disabled={mutation.isPending}>
        {mutation.isPending ? 'Saving...' : 'Save'}
      </Button>
    </form>
  )
}
```

## Error Handling

```typescript
.handler(async ({ input, ctx }) => {
  try {
    const result = await Service.create(ctx.userId, input)
    if (!result.success) {
      throw new Error(result.error)
    }
    return result.data
  } catch (error) {
    // ZSA automatically handles errors
    throw error
  }
})
```

## Rules

- Call Service layer, NOT DAL directly
- Always validate input with Zod schema
- Use `revalidatePath` or `revalidateTag` after mutations
- Never expose internal errors to client
- Keep actions thin - business logic goes in services

Overview

This skill teaches implementing Next.js server actions using ZSA (Zod Server Actions) with built-in authentication, validation, and React Query integration. It provides a consistent file pattern, required directives, and examples for protected and public actions. The guidance focuses on thin action handlers that call service layers, handle errors, and trigger Next.js revalidation when needed.

How this skill works

Actions are server-only files that start with 'use server' and import 'server-only' to prevent client imports. Use authedProcedure for authenticated endpoints or createServerAction for public ones, chain .input(.), .output(.), .onComplete(.), and .handler(.). Client code uses useServerActionMutation and useServerActionQuery to integrate with React Query patterns and form libraries like react-hook-form.

When to use it

  • Creating API endpoints or form handlers that run on the server
  • Implementing create, update, delete, and list mutations with server-side validation
  • Protecting actions that require authenticated user context
  • Triggering Next.js cache revalidation after mutations
  • Integrating server actions with React Query for optimistic UI and caching

Best practices

  • Place actions under features/<feature>/usecases/... following the create/update/delete/list action pattern
  • Always start action files with 'use server' and import 'server-only' as the second line
  • Validate all inputs with Zod schemas and declare outputs where appropriate
  • Use authedProcedure for protected actions so ctx.userId is available
  • Call the service layer from handlers; keep business logic out of actions
  • Use revalidatePath or revalidateTag after successful mutations and avoid exposing internal errors

Example use cases

  • Create a create-<entity>-action.ts that validates formData, calls AccountService.create, revalidates '/accounts' and returns the created entity
  • Implement update-<entity>-action.ts using authedProcedure to ensure only the owner can update
  • Build list-<entity>-action.ts as a read query used via useServerActionQuery on the client for server-rendered lists
  • Wire a React Hook Form create form to useServerActionMutation for submission with zodResolver validation
  • Use onComplete to revalidate pages or tags after delete or update mutations

FAQ

Do action files run on the server or client?

Action files must run on the server. Always include 'use server' and import 'server-only' to prevent client import.

How do I protect an action so only authenticated users can run it?

Use authedProcedure.createServerAction(); ctx.userId will be available in the handler for service calls.

Where should business logic live?

Keep actions thin. Move business logic to service layer functions and call those from the action handler.