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.
'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 }
});
}- 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.
import { createComment } from './actions';
export default function CommentForm() {
return (
<form action={createComment}>
<textarea name='text' required />
<button type='submit'>Post comment</button>
</form>
);
}- Forms provide a simple mutation interface.
- Use meaningful
nameattributes 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.
A disabled button, hidden field, or client-side validation is not a security boundary. Every server mutation must validate authorization and input.
'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);
}- 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.
'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>
);
}- Optimistic updates improve perceived responsiveness.
- Always design a failure or rollback strategy.
- Use optimistic behavior for operations where temporary assumptions are acceptable.