Docs
Dashboard

Mutations

Learn how to write mutations to create, update or delete data.

The dashboard uses Server Actions to handle mutations, making development faster and more efficient.

Server actions

Server actions are a React/Next.js feature that let you write server-side logic callable directly from the client.

To create a server action add the 'use server' directive at the top. This instructs Next.js that the file contains server-side logic, which can be called from the client side. Note that it creates an implicit POST endpoint.

Add a file under apps/dashboard/actions/my-action.ts with the following code:

'use server';
 
import { actionClient } from '~/actions/safe-action';
 
export const myAction = actionClient
  .metadata({ actionName: 'myAction' })
  .action(async () => {
      return 'Hello World';
  });

Now we can use the server actions anywhere in our client components:

'use client';
 
import { Button } from '@workspace/ui/components/button';
 
import { myAction } from '~/actions/my-action';
 
function ClientComponent() {
  const onClick = () => {
    const result = await myAction();
    console.log(result);
  }
 
  return <Button onClick={onClick}>Test</Button>;
}

Input validation

We use Zod to validate any input. This is necessary during runtime, since we can't just rely on the Typescript types which are a compile-time thing.

Add a new schema at apps/dashboard/schemas/my-action-schema.ts;

import { z } from 'zod';
 
export const myActionSchema = z.object({
  name: z.string().min(1)
});
 
export type MyActionSchema = z.infer<typeof myActionSchema>;

Let's use the validation schema:

'use server';
 
import { actionClient } from '~/actions/safe-action';
import { myActionSchema } from '~/schemas/my-action-schema';
 
export const myAction = actionClient
  .metadata({ actionName: 'myAction' })
  .schema(myActionSchema)
  .action(async ({ parsedInput }) => {
      return `Hello ${parsedInput.name}`;
  });

Let's see how it looks like on the client:

'use client';
 
import { Button } from '@workspace/ui/components/button';
 
import { myAction } from '~/actions/my-action';
 
function ClientComponent() {
  const onClick = () => {
    const result = await myAction({ name: 'John Doe' });
    if (result?.serverError) {
      console.log("Something went wrong.");
    }
     if (result?.validationErrors) {
      console.log("Validation failed.");
    }
  }
 
  return <Button onClick={onClick}>Test</Button>;
}

We can access server and validation errors. The validation errors are flattened zod errors. Use a ? to check if a result exists in case the server action aborts early.

Authentication Context

Change the client from actionClient to authActionClient:

'use server';
 
import { prisma } from '@workspace/database/client';
 
import { authActionClient } from '~/actions/safe-action';
import { myActionSchema } from '~/schemas/my-action-schema';
 
export const myAction = authActionClient
  .metadata({ actionName: 'myAction' })
  .schema(myActionSchema)
  .action(async ({ parsedInput, ctx }) => {
      const session = ctx.session;
      const user = ctx.session.user;
 
      return `Hello ${parsedInput.name}`;
  });

Now the action will redirect unauthenticated users and you have access to the session object.

Authentication organization context

Similar concept, change the client to authOrganizationActionClient:

'use server';
 
import { authOrganizationActionClient } from '~/actions/safe-action';
import { myActionSchema } from '~/schemas/my-action-schema';
 
export const myAction = authOrganizationActionClient
  .metadata({ actionName: 'myAction' })
  .schema(myActionSchema)
  .action(async ({ parsedInput, ctx }) => {
      const session = ctx.session;
      const user = ctx.session.user;
      const organization = ctx.organization;
      const memberships = ctx.organization.memberships;
 
      return `Hello ${parsedInput.name}`;
  });

The actions will use the slug in the path to derive the active organization. Furthermore the membership is checked. Non-members are redirected to the organizations overview and you have access to the active organization.

Database client

Let's import prisma and do some database operation:

'use server';
 
import { actionClient } from '~/actions/safe-action';
import { myActionSchema } from '~/schemas/my-action-schema';
 
export const myAction = authOrganizationActionClient
  .metadata({ actionName: 'myAction' })
  .schema(myActionSchema)
  .action(async ({ parsedInput, ctx }) => {
      const newItem = await prisma.item.create({
        data: {
          organizationId: ctx.organization.id,
          name: parsedInput.name
        }
      });
 
      return `Hello ${parsedInput.name} with id ${newItem.id}`;
  });

Notice how we pass the organizationId from the context? This is important for security, since it has to be user-unobtainable data. Never use an organizationId or userId through parsedInput.

The same applies to updating:

'use server';
 
import { actionClient } from '~/actions/safe-action';
import { myActionSchema } from '~/schemas/my-action-schema';
 
export const myAction = authOrganizationActionClient
  .metadata({ actionName: 'myAction' })
  .schema(myActionSchema)
  .action(async ({ parsedInput, ctx }) => {
      await prisma.item.update({
        where: {
          id: parsedInput.id,
          organizationId: ctx.organization.id,
        },
        data: {
          name: parsedInput.name
        }
      });
  });

and deleting an item:

'use server';
 
import { actionClient } from '~/actions/safe-action';
import { myActionSchema } from '~/schemas/my-action-schema';
 
export const myAction = authOrganizationActionClient
  .metadata({ actionName: 'myAction' })
  .schema(myActionSchema)
  .action(async ({ parsedInput, ctx }) => {
      await prisma.item.delete({
        where: {
          id: parsedInput.id,
          organizationId: ctx.organization.id,
        }
      });
  });

Route handlers

In rare cases, such as when you need to return binary data, you can fall back to using route handlers. For example, in the starter kit, we use POST route handlers for Excel and CSV exports.