# Server Actions

> Build type-safe server actions with authentication middleware in your Next.js Prisma kit application.

*Canonical: https://makerkit.dev/docs/nextjs-prisma/development-guide/server-actions*

---

Handle form submissions and data mutations with type-safe server actions - async functions that run on the server, validate input with Zod, and enforce authentication through middleware.

Server actions in the Next.js Prisma kit use `next-safe-action` with pre-configured middleware for authentication and authorization. You define a Zod schema for input validation, chain middleware for auth checks, and write your business logic. The framework handles serialization, error propagation, and session management automatically.

Server actions are async functions marked with `'use server'` that execute on the server when called from client components. They eliminate the need for API routes for most mutations.

- **Use server actions when:** handling form submissions, creating/updating/deleting records, or performing any mutation that requires server-side logic and authentication.
- **Use API routes instead when:** building webhooks, serving external APIs, handling non-form requests, or needing custom request/response control.

## Quick Reference

| Client | Middleware |
|--------|------------|
| `authenticatedActionClient` | Requires valid session |
| `adminActionClient` | Requires admin role |
| `organizationActionClient` | Requires org membership |

## Key Benefits

- **Type safety** - Full TypeScript support from client to server
- **Automatic serialization** - Arguments and return values are serialized automatically
- **Built-in error handling** - Errors propagate back to the client predictably
- **Session management** - Access the authenticated user through middleware

## Basic Server Action

At its simplest, a server action is an async function marked with `'use server'`. Always verify authentication before performing operations:

```typescript {% title="app/[locale]/(internal)/_lib/server/my-action.ts" %}
'use server';

import { getSession } from '@kit/better-auth/context';

export async function myAction(data: { name: string }) {
  const session = await getSession();

  if (!session) {
    throw new Error('Unauthorized');
  }

  // Perform operation
  return { success: true };
}
```

## With Validation

Never trust client input. Use Zod schemas to validate data before processing. The `authenticatedActionClient` from `@kit/action-middleware` handles authentication automatically, so you can focus on business logic:

```typescript {% title="app/[locale]/(internal)/_lib/server/create-user.ts" %}
'use server';

import { z } from 'zod';

import { authenticatedActionClient } from '@kit/action-middleware';
import { db } from '@kit/database';

const schema = z.object({
  name: z.string().min(3),
  email: z.string().email(),
});

export const createUserAction = authenticatedActionClient
  .inputSchema(schema)
  .action(async ({ parsedInput: validated, ctx }) => {
    // ctx.user available from middleware
    // Input already validated by schema
    const user = await db.user.create({ data: validated });

    return user;
  });
```

## Error Handling

Server actions need careful error handling. One common pitfall: Next.js redirects throw special errors that must propagate - catching them breaks navigation:

```typescript
'use server';

import { isRedirectError } from 'next/dist/client/components/redirect-error';

export async function myAction() {
  try {
    await someOperation();
    return { success: true };
  } catch (error) {
    // Don't catch redirects
    if (isRedirectError(error)) {
      throw error;
    }

    console.error('Action failed:', error);
    return { 
      success: false, 
      error: 'Operation failed' 
    };
  }
}
```

## With Redirects

Redirecting after a successful operation is common - creating a project should navigate to it. Call `redirect()` after your mutation:

```typescript
'use server';

import { redirect } from 'next/navigation';

export async function createProject(data: ProjectInput) {
  const project = await db.project.create({ data });

  // Redirect to project page
  redirect(`/projects/${project.id}`);
}
```

When calling server actions that redirect from client components, let redirect errors propagate. Catching them prevents navigation:

```typescript
'use client';

import { isRedirectError } from 'next/dist/client/components/redirect-error';

const onSubmit = async (data) => {
  try {
    await createProject(data);
  } catch (error) {
    if (isRedirectError(error)) {
      return;
    }

    // Handle other errors
    toast.error('Failed to create project');
  }
};
```

## Using next-safe-action

The codebase uses `next-safe-action` for enhanced server actions. It provides input validation, middleware support, and consistent return types:

```typescript
'use server';

import { z } from 'zod';

import { authenticatedActionClient } from '@kit/action-middleware';

const schema = z.object({
  name: z.string(),
});

export const myAction = authenticatedActionClient
  .inputSchema(schema)
  .action(async ({ parsedInput, ctx }) => {
    // ctx.user available from middleware
    // parsedInput is validated input
    return { success: true };
  });
```

## Multi-tenancy Pattern

In a multi-tenant application, every operation must be scoped to the current organization. The `organizationActionClient` provides the organization context, or use `withFeaturePermission` to verify specific permissions:

```typescript
'use server';

import { z } from 'zod';

import { organizationActionClient } from '@kit/action-middleware';
import { db } from '@kit/database';

const schema = z.object({
  organizationId: z.string(),
});

export const getOrganizationDataAction = organizationActionClient
  .inputSchema(schema)
  .action(async ({ parsedInput, ctx }) => {
    // ctx.organizationId and ctx.role available from middleware
    // User is verified as a member of the organization

    return db.project.findMany({
      where: { organizationId: ctx.organizationId },
    });
  });
```

## Permission Checks

Authorization should be declarative. Use `withFeaturePermission` for resource-based checks or `withMinRole` for role hierarchy checks:

```typescript
'use server';

import { z } from 'zod';

import { authenticatedActionClient, withFeaturePermission, withMinRole } from '@kit/action-middleware';
import { db } from '@kit/database';

const schema = z.object({
  projectId: z.string(),
});

// Using resource:action permission check
export const deleteProjectAction = authenticatedActionClient
  .use(withFeaturePermission({ project: ['delete'] }))
  .inputSchema(schema)
  .action(async ({ parsedInput: { projectId }, ctx }) => {
    // Permission already verified by middleware
    await db.project.delete({ where: { id: projectId } });
  });

// Or using minimum role check
export const adminOnlyAction = authenticatedActionClient
  .use(withMinRole('admin'))
  .inputSchema(schema)
  .action(async ({ parsedInput, ctx }) => {
    // Only admins and owners can reach here
    // ctx.role is guaranteed to be 'admin' or higher
  });
```

## Common Pitfalls

- **Missing `'use server'` directive**: The directive must be at the top of the file. Without it, the function runs on the client and fails.
- **Catching redirect errors**: Next.js redirects throw special errors. If you catch them, navigation breaks. Use `isRedirectError()` to rethrow.
- **Rolling your own auth**: Use `authenticatedActionClient` instead of manually checking sessions. The middleware is battle-tested.
- **Forgetting organization scope**: Every query must filter by `organizationId`. Without it, users can access other organizations' data.
- **Returning sensitive data**: Server actions return data to the client. Don't include password hashes, API keys, or internal IDs.
- **Large return payloads**: Return only what the client needs. Large objects slow down serialization and increase bundle size.

{% faq
   title="Frequently Asked Questions"
   items=[
     {"question": "When should I use server actions vs API routes?", "answer": "Use server actions for form submissions and mutations from React components. Use API routes for webhooks, external APIs, or when you need custom headers/status codes."},
     {"question": "How do I handle errors on the client?", "answer": "Check result.serverError and result.validationErrors after calling executeAsync(). Use toast.promise for automatic loading/error states."},
     {"question": "Can I redirect after a server action?", "answer": "Yes. Call redirect() at the end of your action. On the client, let the redirect error propagate - don't catch it."},
     {"question": "How do I access the current user?", "answer": "Use authenticatedActionClient. The middleware injects ctx.user with the authenticated user's data."},
     {"question": "Can I chain multiple middleware?", "answer": "Yes. Call .use() multiple times: authenticatedActionClient.use(middleware1).use(middleware2).action(...). They run in order."}
   ]
/%}

## Related

- [Development Guide Overview](./overview) - Full development patterns guide
- [Action Middleware](./action-middleware) - Authentication and authorization configuration
- [Working with Forms](./working-with-forms) - Client-side form integration

---

**Next:** [Action Middleware →](./action-middleware)
