Next.js Server Actions Tutorial: Forms, Mutations & Tips


Get the Next.js weekly drop
Free weekly notes on App Router, auth, and frontend stacks that ship. No spam—just what's worth your inbox.
By subscribing you agree to our Privacy Policy.
Server Actions in Next.js are async server functions you call from the client—forms, mutations, and secure data work without hand-rolling a Route Handler for every submit. They run on the server, cut client JS for those paths, and fit the App Router mental model: mutate, then revalidate.
This tutorial walks through practical Server Actions: forms, mutations, validation, revalidation, and a few pitfalls people hit when wiring auth. Less theory, more patterns you can paste.
A Server Action is a function marked with "use server" that runs only on the server but can be invoked from Client Components (or from forms via action). Use them to write data, call your DB, and return a result—without exposing secrets to the browser.
To get started, you'll need a Next.js project. If you haven't already set one up, you can create a new project with the following command:
npx create-next-app@latest my-nextjs-app
cd my-nextjs-appNext, let's create a server action. Server Actions are functions that run on the server but are invoked from the client. These functions are written in app directory files and are defined using the async keyword.
// app/server/login
'use server';
import { someDatabaseFunction } from 'database'; //Your database
export async function loginForm(data) {
// Perform server-side logic here, like database operations
const result = await someDatabaseFunction(data);
if(!result) throw new Error('Invalid credentials');
return result;
}Now that you have a server action ready, let's create a form in your component that will send data to this server action.
'use client';
import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button';
export default function ContactForm() {
const router = useRouter();
const handleSubmit = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const result = await submitForm({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message')
});
if (result.success) {
router.push('/thank-you');
} else {
console.error('Submission failed');
}
};
return (
<form onSubmit={handleSubmit}>
<input type='text' name='name' placeholder='Your Name' required />
<input type='email' name='email' placeholder='Your Email' required />
<textarea name='message' placeholder='Your Message' required></textarea>
<Button type='submit'>Submit</Button>
</form>
);
}Upon completion of data processing by the server action, it sends a response. In the given example, we evaluate the response to determine if the submission was successful. If it was, we redirect the user to a thank-you page.
Make your Next.js forms look great with Shadcn components. it provides a library of ready-to-use UI elements that can be effortlessly integrated into your forms, offering an aesthetically pleasing and professional user experience.
In this example, we use zod for schema validation, react-hook-form for handling form state, and Shadcn UI components for the form interface. This setup provides a robust, type-safe form handling mechanism in Next.js.
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { loginForm } from "@/app/server/login"
const FormSchema = z.object({
email: z.string().email({ message: "Please enter a valid email" }),
name: z.string().min(1, { message: "Please enter your name" }),
})
export function InputForm() {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
email: "",
name: ""
},
})
async function onSubmit(data: z.infer<typeof FormSchema>) {
try {
console.log(data)
const result = await loginForm(data)
if(result){
toast.success("Login success")
}else {
toast.error("Invalid credentials, please verify your email.")
}
} catch (error) {
if (error instanceof Error) {
toast.error(error.message)
}
}
}
return (
<Card>
<CardHeader>
<CardTitle>Input Form</CardTitle>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="shadcn" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Your name</FormLabel>
<FormControl>
<Input type="name" placeholder="shadcn" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</CardContent>
</Card>
)
}Form more info for the Building forms with React Hook Form and Zod, follow the page Shadcn UI form with more example.
Next.js Server Actions allow developers to seamlessly execute server-side tasks directly from the front end. This simplifies the development process and enables the creation of sophisticated and interactive web applications that combine exceptional design and functionality. They work seamlessly with Shadow Components and other components to elevate the user experience.
This guide has equipped you with the knowledge to use Server Actions in Next.js. Explore further and explore the potential of Next.js. Their capabilities will enhance your web development skills and unlock countless opportunities.