devnotes.

Server Actions & Mutating Data

What is a Server Action?

A Server Action is an async function marked with 'use server' that runs only on the server but can be called directly from a Client or Server Component — no need to manually write a fetch() call to an API route.

// app/actions.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function addStudent(formData: FormData) {
  const name = formData.get('name') as string;
  await db.students.create({ data: { name } });
  revalidatePath('/students');
}

Using a Server Action directly in a <form>

// app/students/new/page.tsx (Server Component)
import { addStudent } from '@/app/actions';

export default function NewStudentPage() {
  return (
    <form action={addStudent}>
      <input name="name" placeholder="Student name" required />
      <button type="submit">Add Student</button>
    </form>
  );
}

This works even with JavaScript disabled — the <form> posts normally — and is progressively enhanced with client-side transitions when JS is available.

Combining with React 19's useActionState

For validation errors and pending state in a Client Component:

'use client';
import { useActionState } from 'react';
import { addStudent } from '@/app/actions';

const initialState = { error: null };

export default function StudentForm() {
  const [state, formAction, isPending] = useActionState(addStudent, initialState);

  return (
    <form action={formAction}>
      <input name="name" required />
      {state?.error && <p className="error">{state.error}</p>}
      <button disabled={isPending}>{isPending ? 'Saving...' : 'Add'}</button>
    </form>
  );
}
// updated action to return validation state
'use server';
export async function addStudent(prevState: unknown, formData: FormData) {
  const name = formData.get('name') as string;
  if (!name) return { error: 'Name is required' };
  await db.students.create({ data: { name } });
  revalidatePath('/students');
  return { error: null };
}

Calling a Server Action from an event handler

Server Actions aren't limited to forms — call them from any event, like a delete button:

'use client';
import { deleteStudent } from '@/app/actions';

function DeleteButton({ id }: { id: number }) {
  return <button onClick={() => deleteStudent(id)}>Delete</button>;
}

Security note

Server Actions create a public HTTP endpoint under the hood, so always re-validate input and re-check permissions/authentication inside the action itself — never trust that a button being hidden in the UI is enough protection.

Next.js Server Actions: Handling Forms and Mutations | DevNotes