HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
4 Topics
Chapter•100 Minutes

Server Actions, Forms and Mutations

Learn how to implement secure server-side mutations using Server Actions, forms, validation, optimistic interfaces, and cache invalidation.

What Are Server Actions?

Server Actions provide a way to define functions that execute on the server and can be invoked from supported React interactions such as forms.

They are useful for operations such as creating records, updating profiles, deleting resources, submitting feedback, or changing account settings.

The major advantage is that the mutation logic can remain on the server instead of requiring a manually created HTTP endpoint for every form operation.

Basic Server Action
'use server';

export async function createComment(formData: FormData) {
  const text = formData.get('text');

  if (typeof text !== 'string' || !text.trim()) {
    throw new Error('Comment is required');
  }

  await db.comment.create({
    data: { text }
  });
}
💡 The action executes on the server and receives submitted form data.
Key Takeaways
  • Server Actions are designed for server-side mutations.
  • They can simplify form submission architecture.
  • Actions still require authentication and authorization.

Progressive Form Handling

Forms are a natural interface for server mutations. Instead of writing a client-side fetch request for every form, an action can be attached to the form submission.

This approach can work even before extensive client-side JavaScript is required. Interactive enhancements can then be layered on top.

Form Using an Action
import { createComment } from './actions';

export default function CommentForm() {
  return (
    <form action={createComment}>
      <textarea name='text' required />
      <button type='submit'>Post comment</button>
    </form>
  );
}
💡 The browser submits the form to the server-side action.
Key Takeaways
  • Forms provide a simple mutation interface.
  • Use meaningful name attributes so FormData contains expected fields.
  • Client-side validation can improve UX but must not replace server validation.

Validation and Security

Server Actions are server entry points and should be treated as externally reachable mutation boundaries. Never assume that because a button is hidden in the UI, the action cannot be invoked.

Validate input on the server. Validate types, ranges, formats, ownership, and business rules. Libraries such as Zod can provide reusable schemas.

Authentication answers 'who are you?' Authorization answers 'are you allowed to perform this operation?' Both matter.

Never Trust the UI

A disabled button, hidden field, or client-side validation is not a security boundary. Every server mutation must validate authorization and input.

Server Validation
'use server';

import { z } from 'zod';

const schema = z.object({
  name: z.string().min(2).max(100)
});

export async function updateProfile(formData: FormData) {
  const result = schema.safeParse({
    name: formData.get('name')
  });

  if (!result.success) {
    return { error: 'Invalid name' };
  }

  const user = await requireUser();
  await updateUser(user.id, result.data);
}
💡 The action validates untrusted input before using it and also establishes the current user.
Key Takeaways
  • Validate all mutation input on the server.
  • Check authorization for the specific resource and operation.
  • Treat Server Actions as security-sensitive entry points.

Optimistic UI Updates

Optimistic updates change the interface immediately while the server operation is still running. They are useful when the expected operation is fast and reversible, such as liking a post or toggling a setting.

The UI should have a recovery strategy if the server rejects the mutation. Optimistic UI without error handling can create a mismatch between what the user sees and what the database contains.

Optimistic Interaction
'use client';

import { useOptimistic } from 'react';

export function LikeButton({ liked }: { liked: boolean }) {
  const [optimisticLiked, setOptimisticLiked] = useOptimistic(liked);

  return (
    <button onClick={() => setOptimisticLiked(!optimisticLiked)}>
      {optimisticLiked ? 'Unlike' : 'Like'}
    </button>
  );
}
💡 The optimistic state allows the interface to respond immediately while the server mutation is processed.
Key Takeaways
  • Optimistic updates improve perceived responsiveness.
  • Always design a failure or rollback strategy.
  • Use optimistic behavior for operations where temporary assumptions are acceptable.