FrontendPrep
Menu
Topics
Questions
Guides
Challenges
Soon
Back to Next.js Questions
nextjsMedium

Next.js: Server Actions and Data Mutations

Master Server Actions in Next.js App Router. Learn about forms integration, progressive enhancement, revalidation, optimistic UI updates, and securing database mutations.

Next.js: Server Actions and Data Mutations

One of the most frequent form-handling questions in Next.js interviews is:

What are Server Actions in Next.js, and how do they support progressive enhancement? Explain how to validate schemas, clear caches, and update the UI optimistically.

Server Actions are asynchronous functions executed on the server that can be invoked directly from Client or Server Components. They form the backbone of forms, mutations, and database updates in the App Router.


1. What are Server Actions?

Server Actions are built on React's actions model. They replace conventional API routes (/pages/api/* or /app/api/route.ts) by allowing you to call server-side function endpoints inside event handlers or form actions directly.

  • "use server": Declares that a function or a module is server-only.
  • Client compatibility: Next.js compiles the action into a POST endpoint automatically under the hood.
// app/actions.ts
'use server';
 
export async function createFeedback(formData: FormData) {
  const message = formData.get('message');
  // Save to database
  await db.feedback.create({ message });
}

2. Progressive Enhancement

A key selling point of Server Actions is Progressive Enhancement:

  • When invoked using the HTML <form action={createFeedback}> attribute, the form works even if JavaScript is disabled on the client browser.
  • Once client-side hydration completes, Next.js intercepts the submit event to perform smooth, client-side fetch updates without reloading the page.

3. Form Validation & UI State

In real-world applications, you must validate incoming data and handle errors gracefully.

// app/actions.ts
'use server';
import { z } from 'zod';
 
const schema = z.object({
  email: z.string().email(),
});
 
export async function subscribeNewsletter(prevState: any, formData: FormData) {
  const validation = schema.safeParse({
    email: formData.get('email'),
  });
 
  if (!validation.success) {
    return { error: 'Invalid email address' };
  }
 
  await db.subscribers.create({ email: validation.data.email });
  return { success: true };
}

In the Client Component:

You use the useActionState (formerly useFormState) hook to retrieve errors and submission statuses from Server Actions:

'use client';
import { useActionState } from 'react';
import { subscribeNewsletter } from './actions';
 
export default function SubscribeForm() {
  const [state, formAction, isPending] = useActionState(subscribeNewsletter, null);
 
  return (
    <form action={formAction}>
      <input type="email" name="email" required />
      <button disabled={isPending}>Subscribe</button>
      {state?.error && <p className="text-red-500">{state.error}</p>}
    </form>
  );
}

4. Optimistic UI Updates

To improve perceived performance, you can display output changes instantly using React's useOptimistic hook before the server transaction finishes.

'use client';
import { useOptimistic } from 'react';
 
export function MessagesList({ initialMessages, sendMessageAction }) {
  const [optimisticMessages, addOptimisticMessage] = useOptimistic(
    initialMessages,
    (state, newMessage: string) => [...state, { text: newMessage, sending: true }]
  );
 
  async function action(formData: FormData) {
    const message = formData.get('message') as string;
    addOptimisticMessage(message); // Show instantly
    await sendMessageAction(message); // Send to server
  }
 
  return (
    <form action={action}>
      {optimisticMessages.map((m, idx) => (
        <p key={idx} className={m.sending ? 'opacity-50' : ''}>{m.text}</p>
      ))}
      <input name="message" />
    </form>
  );
}

Key Takeaways

  • useActionState: The standard pattern to handle action feedback loops (errors, messages) in interactive forms.
  • Cache Revalidation: Always call revalidatePath() or revalidateTag() inside server actions to clear page caches immediately after performing mutations.
  • Security Check: Never assume client inputs are safe. Always validate schemas on the server using Zod or custom checks.
  • Server-Only files: If a file defines export functions with 'use server', every function inside compiles into a public API endpoint.

Share this Resource

Help other developers level up by sharing this study guide.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.