What are Server Actions?
Server Actions are asynchronous functions that run on the server. They can be called from Client Components and Server Components, providing a seamless way to perform server-side operations like data mutations, API calls, and database operations.
Server Actions are marked with the "use server" directive and always run on the server, even when called from client-side code.
The “use server” Directive
Every server action file must start with the "use server" directive at the top:
From src/lib/actions/message/createMessage.ts:1:
"use server" ;
import { revalidatePath } from "next/cache" ;
import { z } from "zod" ;
The "use server" directive must be at the very top of the file, before any imports.
Server Action Structure
All server actions in this project follow a consistent pattern:
Define State Interface
Define TypeScript interface for the return state
Create Zod Schema
Define validation schema for input data
Implement Action Function
Create async function that validates, processes, and returns state
Revalidate Cache
Call revalidatePath() to refresh cached data after mutations
Complete Server Action Example
Let’s examine a full server action for creating messages:
From src/lib/actions/message/createMessage.ts:
State Interface
Zod Schema
Action Function
interface MessagePrevState {
success ?: boolean ;
error ?: string ;
}
All server actions use Zod for runtime type validation:
Basic Validation
From src/lib/actions/people/createPerson.ts:11-16:
const personSchema = z . object ({
name: z . string (). min ( 1 , "Name cannot be empty" ),
phone: z
. string ()
. regex ( / ^ 8801 \d {9} $ / , "Invalid phone number format. Must be 8801XXXXXXXXX" ),
});
Validation with safeParse
Use safeParse to validate without throwing errors:
const parsedData = personSchema . safeParse ({
name: formData . get ( "name" ),
phone: formData . get ( "phone" ),
});
if ( ! parsedData . success ) {
return {
error: parsedData . error . errors . map (( err ) => err . message ). join ( ", " ),
};
}
// parsedData.data is now type-safe
const { name , phone } = parsedData . data ;
safeParse returns an object with a success boolean and either data (if valid) or error (if invalid).
Complex Validation
From src/lib/actions/message/updateMessage.ts:11-19:
const messageSchema = z . object ({
id: z . string (). min ( 1 , "ID is required" ),
content: z . string (). min ( 1 , "Content cannot be empty" ),
sendToPhone: z
. string ()
. regex ( / ^ 8801 \d {9} $ / , "Invalid phone number format. Must be 8801XXXXXXXXX" ),
sendAfter: z
. number (). min ( 0 , "Send after is required and minimum after 0 day" ),
});
Using Server Actions in Client Components
Server Actions integrate seamlessly with the useActionState hook (React 19):
From src/components/CreateMessageForm.tsx:22-39:
"use client" ;
import { useActionState , useEffect } from "react" ;
import createMessage from "@/lib/actions/message/createMessage" ;
import { toast } from "sonner" ;
export default function MessageUpdateForm ({ people } : { people : People [] }) {
const [ createState , createAction , createIsPending ] = useActionState (
createMessage ,
{}
);
useEffect (() => {
if ( createState . success ) {
toast . success ( "Message created successfully" , {
richColors: true ,
});
}
if ( createState . error ) {
toast . error ( "Message creation failed" , {
description: createState . error ,
duration: 3500 ,
richColors: true ,
});
}
}, [ createState ]);
return (
< form action = { createAction } className = "space-y-3 flex flex-col" >
< Textarea name = "content" />
< Button type = "submit" disabled = { createIsPending } >
Add
</ Button >
</ form >
);
}
useActionState Hook
The useActionState hook returns three values:
createState Current state returned from the server action
createAction Function to pass to form’s action prop
createIsPending Boolean indicating if action is currently running
The revalidatePath Pattern
After mutating data, you must revalidate the cache to reflect changes:
From src/lib/actions/message/createMessage.ts:58-59:
revalidatePath ( "/" );
return { success: true };
From src/lib/actions/reschedule/reschedule.ts:24-25:
revalidatePath ( "/" );
return { success: true };
revalidatePath("/") tells Next.js to re-fetch all data for the home page, ensuring the UI reflects the latest database state.
Why revalidatePath is Important
Cache Invalidation
Next.js caches Server Component renders for performance. revalidatePath clears this cache.
Automatic Re-render
After cache invalidation, Server Components automatically re-fetch data and re-render.
Consistent UI
Users see updated data immediately without manual page refresh.
Error Handling Pattern
All server actions use consistent error handling:
try {
// Validation
const parsedData = schema . safeParse ( data );
if ( ! parsedData . success ) {
return { error: "Validation error message" };
}
// API call
const response = await fetch ( url , options );
if ( ! response . ok ) {
return { error: `Backend Error: ${ response . status } ` };
}
// Success
revalidatePath ( "/" );
return { success: true };
} catch ( error ) {
return {
error: error instanceof Error ? error . message : "Something went wrong" ,
};
}
Always return error messages to the client instead of throwing errors. This provides better user experience and debugging information.
Server Actions can be called directly without forms:
From src/lib/actions/reschedule/reschedule.ts:10-29:
"use server"
import { revalidatePath } from "next/cache" ;
interface RescheduleState {
success : boolean ;
error ?: string ;
}
export default async function reschedule () : Promise < RescheduleState > {
try {
const response = await fetch ( ` ${ process . env . BACKEND_URL } /messages/reschedule` , {
method: "GET" ,
headers: {
Authorization: `Basic ${ Buffer . from (
` ${ process . env . USERNAME } : ${ process . env . PASSWORD } `
). toString ( "base64" ) } ` ,
},
})
if ( ! response . ok ) {
return { success: false };
}
revalidatePath ( "/" );
return { success: true };
} catch ( e ) {
return { success: false , error: e instanceof Error ? e . message : "Unknown error" };
}
}
This action takes no form data and can be triggered by a button click.
Real-World Examples
From src/lib/actions/people/createPerson.ts:"use server" ;
import { revalidatePath } from "next/cache" ;
import { z } from "zod" ;
interface PersonPrevState {
success ?: boolean ;
error ?: string ;
}
const personSchema = z . object ({
name: z . string (). min ( 1 , "Name cannot be empty" ),
phone: z
. string ()
. regex ( / ^ 8801 \d {9} $ / , "Invalid phone number format. Must be 8801XXXXXXXXX" ),
});
export default async function createPerson (
prevState : PersonPrevState ,
formData : FormData
) : Promise < PersonPrevState > {
try {
const parsedData = personSchema . safeParse ({
name: formData . get ( "name" ),
phone: formData . get ( "phone" ),
});
if ( ! parsedData . success ) {
return {
error: parsedData . error . errors . map (( err ) => err . message ). join ( ", " ),
};
}
const response = await fetch (
` ${ process . env . BACKEND_URL } /people/create-one` ,
{
method: "POST" ,
headers: {
Authorization: `Basic ${ Buffer . from (
` ${ process . env . USERNAME } : ${ process . env . PASSWORD } `
). toString ( "base64" ) } ` ,
"Content-Type" : "application/json" ,
},
body: JSON . stringify ( parsedData . data ),
}
);
if ( ! response . ok ) {
return {
error: `Backend Error: ${ response . status } ${ response . statusText } ` ,
};
}
revalidatePath ( "/" );
return { success: true };
} catch ( error ) {
return {
error: error instanceof Error ? error . message : "Something went wrong" ,
};
}
}
From src/lib/actions/message/updateMessage.ts:21-66:export default async function updateMessage (
prevState : MessagePrevState ,
formData : FormData
) : Promise < MessagePrevState > {
try {
const parsedData = messageSchema . safeParse ({
id: formData . get ( "id" ),
content: formData . get ( "content" ),
sendToPhone: formData . get ( "sendToPhone" ),
sendAfter: Number ( formData . get ( "sendAfter" )),
});
if ( ! parsedData . success ) {
return {
error: parsedData . error . errors . map (( err ) => err . message ). join ( ", " ),
};
}
const response = await fetch (
` ${ process . env . BACKEND_URL } /messages/update-one/ ${ parsedData . data . id } ` ,
{
method: "POST" ,
headers: {
Authorization: `Basic ${ Buffer . from (
` ${ process . env . USERNAME } : ${ process . env . PASSWORD } `
). toString ( "base64" ) } ` ,
"Content-Type" : "application/json" ,
},
body: JSON . stringify ( parsedData . data ),
}
);
if ( ! response . ok ) {
return {
error: `Backend Error: ${ response . status } ${ response . statusText } ` ,
};
}
revalidatePath ( "/" );
return { success: true };
} catch ( error ) {
return {
error: error instanceof Error ? error . message : "Something went wrong" ,
};
}
}
Best Practices
Always Validate Use Zod schemas to validate all input data before processing
Return State Always return success/error state instead of throwing errors
Revalidate Cache Call revalidatePath() after data mutations to update the UI
Type Safety Define TypeScript interfaces for state and use Zod for runtime validation
Next Steps
Authentication Learn how authentication works with Server Actions
API Reference View all available Server Actions