What are Server Actions?
Server Actions are asynchronous functions that run on the server . They enable you to handle form submissions and data mutations without building a separate API.
Server Actions are marked with the 'use server' directive and can be called from both Server Components and Client Components.
Key Benefits
No API Routes Handle mutations without creating API endpoints
Progressive Enhancement Forms work without JavaScript enabled
Type Safety Full TypeScript support end-to-end
Revalidation Automatically revalidate cached data
Creating a Server Action
There are two ways to define Server Actions:
Inline Server Action
Define the action directly in a Server Component:
import ImagePicker from '@/components/meals/image-picker' ;
import classes from './page.module.css' ;
export default function ShareMealPage () {
async function shareMeal ( formData ) {
'use server' ;
const meal = {
title: formData . get ( 'title' ),
summary: formData . get ( 'summary' ),
instructions: formData . get ( 'instructions' ),
image: formData . get ( 'image' ),
creator: formData . get ( 'name' ),
creator_email: formData . get ( 'email' )
}
console . log ( meal );
}
return (
<>
< header className = { classes . header } >
< h1 >
Share your < span className = { classes . highlight } > favorite meal </ span >
</ h1 >
< p > Or any other meal you feel needs sharing! </ p >
</ header >
< main className = { classes . main } >
< form className = { classes . form } action = { shareMeal } >
< div className = { classes . row } >
< p >
< label htmlFor = "name" > Your name </ label >
< input type = "text" id = "name" name = "name" required />
</ p >
< p >
< label htmlFor = "email" > Your email </ label >
< input type = "email" id = "email" name = "email" required />
</ p >
</ div >
< p >
< label htmlFor = "title" > Title </ label >
< input type = "text" id = "title" name = "title" required />
</ p >
< p >
< label htmlFor = "summary" > Short Summary </ label >
< input type = "text" id = "summary" name = "summary" required />
</ p >
< p >
< label htmlFor = "instructions" > Instructions </ label >
< textarea
id = "instructions"
name = "instructions"
rows = "10"
required
></ textarea >
</ p >
< ImagePicker label = "Your image" name = "image" />
< p className = { classes . actions } >
< button type = "submit" > Share Meal </ button >
</ p >
</ form >
</ main >
</>
);
}
User fills out the form
Form submits to the shareMeal action
NextJS automatically serializes form data
Function executes on the server
Console.log appears in your terminal (not browser!)
Separate Server Actions File
For better organization and reusability, create a separate actions file:
'use server' ;
export async function shareMeal ( formData ) {
const meal = {
title: formData . get ( 'title' ),
summary: formData . get ( 'summary' ),
instructions: formData . get ( 'instructions' ),
image: formData . get ( 'image' ),
creator: formData . get ( 'name' ),
creator_email: formData . get ( 'email' ),
};
console . log ( meal );
}
When using 'use server' at the file level, all exported functions in that file become Server Actions.
Then import and use it:
import { shareMeal } from '@/lib/actions' ;
export default function ShareMealPage () {
return (
< form action = { shareMeal } >
{ /* form fields */ }
</ form >
);
}
Real-World Example: User Management
Here’s a complete example from the course:
'use server' ;
import fs from 'node:fs' ;
export async function saveUserAction ( formData ) {
console . log ( 'Executed' );
const data = fs . readFileSync ( 'dummy-db.json' , 'utf-8' );
const instructors = JSON . parse ( data );
const newInstructor = {
id: new Date (). getTime (). toString (),
name: formData . get ( 'name' ),
title: formData . get ( 'title' ),
};
instructors . push ( newInstructor );
fs . writeFileSync ( 'dummy-db.json' , JSON . stringify ( instructors ));
}
Using in a Client Component
components/ServerActionsDemo.js
'use client' ;
import { saveUserAction } from '@/actions/users' ;
export default function ServerActionsDemo () {
return (
< div className = "rsc" >
< h2 > Server Actions </ h2 >
< p >
A "Form Action" converted to a "Server Action" via { ' ' }
< strong > "use server" </ strong > .
</ p >
< p > Can be defined in a server component or a separate file. </ p >
< p > Can be called from inside server component or client component. </ p >
< form action = { saveUserAction } >
< p >
< label htmlFor = "name" > User name </ label >
< input type = "text" id = "name" name = "name" required />
</ p >
< p >
< label htmlFor = "title" > Title </ label >
< input type = "text" id = "title" name = "title" required />
</ p >
< p >
< button > Save User </ button >
</ p >
</ form >
</ div >
);
}
Server Actions can be called from both Server Components and Client Components. The function always executes on the server.
Add server-side validation to protect your data:
'use server' ;
import { redirect } from 'next/navigation' ;
import { saveMeal } from './meals' ;
function isInvalidText ( text ) {
return ! text || text . trim () === '' ;
}
export async function shareMeal ( formData ) {
const meal = {
title: formData . get ( 'title' ),
summary: formData . get ( 'summary' ),
instructions: formData . get ( 'instructions' ),
image: formData . get ( 'image' ),
creator: formData . get ( 'name' ),
creator_email: formData . get ( 'email' ),
};
if (
isInvalidText ( meal . title ) ||
isInvalidText ( meal . summary ) ||
isInvalidText ( meal . instructions ) ||
isInvalidText ( meal . creator ) ||
isInvalidText ( meal . creator_email ) ||
! meal . creator_email . includes ( '@' ) ||
! meal . image ||
meal . image . size === 0
) {
throw new Error ( 'Invalid input' );
}
await saveMeal ( meal );
redirect ( '/meals' );
}
Always validate on the server! Client-side validation can be bypassed. Server-side validation is your security layer.
Use the useFormState hook to return validation errors to the user:
'use server' ;
import { redirect } from 'next/navigation' ;
import { saveMeal } from './meals' ;
function isInvalidText ( text ) {
return ! text || text . trim () === '' ;
}
export async function shareMeal ( prevState , formData ) {
const meal = {
title: formData . get ( 'title' ),
summary: formData . get ( 'summary' ),
instructions: formData . get ( 'instructions' ),
image: formData . get ( 'image' ),
creator: formData . get ( 'name' ),
creator_email: formData . get ( 'email' ),
};
if (
isInvalidText ( meal . title ) ||
isInvalidText ( meal . summary ) ||
isInvalidText ( meal . instructions ) ||
isInvalidText ( meal . creator ) ||
isInvalidText ( meal . creator_email ) ||
! meal . creator_email . includes ( '@' ) ||
! meal . image ||
meal . image . size === 0
) {
return {
message: 'Invalid input.' ,
};
}
await saveMeal ( meal );
redirect ( '/meals' );
}
Key Changes for useFormState
Using in Component
components/ShareMealForm.js
'use client' ;
import { useFormState } from 'react-dom' ;
import { shareMeal } from '@/lib/actions' ;
export default function ShareMealForm () {
const [ state , formAction ] = useFormState ( shareMeal , { message: null });
return (
< form action = { formAction } >
{ state . message && < p className = "error" > { state . message } </ p > }
< p >
< label htmlFor = "title" > Title </ label >
< input type = "text" id = "title" name = "title" required />
</ p >
{ /* other form fields */ }
< button type = "submit" > Share Meal </ button >
</ form >
);
}
Show loading state during form submission:
components/SubmitButton.js
'use client' ;
import { useFormStatus } from 'react-dom' ;
export default function SubmitButton () {
const { pending } = useFormStatus ();
return (
< button type = "submit" disabled = { pending } >
{ pending ? 'Submitting...' : 'Share Meal' }
</ button >
);
}
useFormStatus must be used in a component that is a child of the form, not in the component that renders the form itself.
Cache Revalidation
Invalidate cached data after mutations:
'use server' ;
import { revalidatePath } from 'next/cache' ;
import { redirect } from 'next/navigation' ;
import { saveMeal } from './meals' ;
export async function shareMeal ( prevState , formData ) {
// ... validation and save logic
await saveMeal ( meal );
// Revalidate the meals page cache
revalidatePath ( '/meals' );
redirect ( '/meals' );
}
Submit Form
User submits the form with new meal data
Server Action Runs
shareMeal validates and saves the meal
Cache Invalidation
revalidatePath('/meals') marks the meals cache as stale
Redirect
User redirected to meals page
Fresh Data
Page re-renders with updated data
Revalidation Options
Revalidate Specific Path
Revalidate All Paths
Revalidate by Tag
revalidatePath ( '/meals' );
Progressive Enhancement
Server Actions work even if JavaScript is disabled:
< form action = { shareMeal } >
< input type = "text" name = "title" required />
< button type = "submit" > Submit </ button >
</ form >
This form will work with or without JavaScript! NextJS handles the submission server-side automatically.
Error Handling
Handle errors gracefully in Server Actions:
'use server' ;
export async function shareMeal ( prevState , formData ) {
try {
const meal = {
title: formData . get ( 'title' ),
// ... other fields
};
// Validation
if ( isInvalidText ( meal . title )) {
return {
message: 'Invalid input.' ,
};
}
await saveMeal ( meal );
revalidatePath ( '/meals' );
redirect ( '/meals' );
} catch ( error ) {
return {
message: 'Failed to save meal. Please try again.' ,
};
}
}
Best Practices
Always Validate on Server
Never trust client data. Validate all inputs in your Server Actions.
Keep Server Actions in dedicated files (e.g., lib/actions.js) for better organization and reusability.
Use useFormState to return specific error messages that help users fix issues.
Revalidate After Mutations
Always call revalidatePath() or revalidateTag() after data changes to keep cache fresh.
Use useFormStatus to provide feedback during form submission.
Server Actions vs API Routes
Feature Server Actions API Routes Setup Minimal (just 'use server') Create separate files Type Safety Full end-to-end Manual type definitions Form Integration Native HTML forms Requires fetch/axios Progressive Enhancement Yes No Revalidation Built-in Manual Use Case Form submissions, mutations Public APIs, webhooks
Use Server Actions for internal data mutations. Use API Routes when you need a public API endpoint or webhook.
Complete Example: Full CRUD Flow
Create Action File
'use server' ;
export async function createMeal ( formData ) { /* ... */ }
export async function updateMeal ( id , formData ) { /* ... */ }
export async function deleteMeal ( id ) { /* ... */ }
Build Form Component
'use client' ;
import { useFormState } from 'react-dom' ;
import { createMeal } from '@/lib/actions' ;
// Component code...
Add Validation
Validate inputs server-side and return errors
Revalidate Cache
Invalidate affected pages after mutations
Redirect
Navigate user to success page
Next Steps
Database Integration Connect Server Actions to PostgreSQL, MongoDB, or other databases
File Uploads Handle file uploads in Server Actions
Authentication Protect Server Actions with authentication
Optimistic Updates Update UI immediately before server confirmation