- Published on
Next.js Server Actions - The End of API Routes?
- Authors

- Name
- Sanjeev Sharma
- @webcoderspeed1
Introduction
Next.js Server Actions let you run server-side code directly from React components — no API routes needed. Mutate your database, send emails, update sessions — all from a function call in your UI.
In 2026, Server Actions have matured into the recommended way to handle form submissions and data mutations in Next.js.
- What Are Server Actions?
- Your First Server Action
- Using Server Actions with Forms Directly
- useFormState and useFormStatus (React 19)
- Inline Server Actions in Server Components
- Validation with Zod
- Authentication in Server Actions
- Optimistic Updates with useOptimistic
- Conclusion
What Are Server Actions?
Server Actions are async functions that run on the server but can be called from Client Components. They're defined with the 'use server' directive.
Client Component → calls Server Action → runs on server → returns result
No fetch, no API route, no JSON parsing — just a function call.
Your First Server Action
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function createPost(title: string, content: string) {
await db.post.create({
data: { title, content }
})
revalidatePath('/blog') // Refresh the blog page cache
}
'use client'
import { createPost } from '@/app/actions'
export default function NewPostPage() {
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault()
const formData = new FormData(e.currentTarget)
await createPost(
formData.get('title') as string,
formData.get('content') as string
)
}
return (
<form onSubmit={handleSubmit}>
<input name="title" placeholder="Post title" />
<textarea name="content" placeholder="Content" />
<button type="submit">Create Post</button>
</form>
)
}
Using Server Actions with Forms Directly
The cleanest pattern — bind a Server Action directly to a <form>:
import { submitContact } from '@/app/actions'
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<button type="submit">Send</button>
</form>
)
}
'use server'
export async function submitContact(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
await sendEmail({ name, email, message })
redirect('/contact/success')
}
No preventDefault, no useState for loading — it just works!
useFormState and useFormStatus (React 19)
For better UX with loading and error states:
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { createPost } from '@/app/actions'
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button type="submit" disabled={pending}>
{pending ? 'Creating...' : 'Create Post'}
</button>
)
}
export default function NewPostPage() {
const [state, formAction] = useFormState(createPost, null)
return (
<form action={formAction}>
{state?.error && <p className="error">{state.error}</p>}
{state?.success && <p className="success">Post created!</p>}
<input name="title" required />
<textarea name="content" required />
<SubmitButton />
</form>
)
}
'use server'
export async function createPost(prevState: any, formData: FormData) {
try {
const title = formData.get('title') as string
if (!title) return { error: 'Title is required' }
await db.post.create({ data: { title } })
revalidatePath('/blog')
return { success: true }
} catch (e) {
return { error: 'Something went wrong' }
}
}
Inline Server Actions in Server Components
You can define server actions inline inside Server Components:
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export default async function TodosPage() {
const todos = await db.todo.findAll()
async function addTodo(formData: FormData) {
'use server' // Inline 'use server' directive
const text = formData.get('text') as string
await db.todo.create({ data: { text } })
revalidatePath('/todos')
}
async function deleteTodo(id: string) {
'use server'
await db.todo.delete({ where: { id } })
revalidatePath('/todos')
}
return (
<div>
<form action={addTodo}>
<input name="text" placeholder="New todo" />
<button type="submit">Add</button>
</form>
<ul>
{todos.map(todo => (
<li key={todo.id}>
{todo.text}
<form action={deleteTodo.bind(null, todo.id)}>
<button type="submit">Delete</button>
</form>
</li>
))}
</ul>
</div>
)
}
Validation with Zod
Always validate inputs server-side:
'use server'
import { z } from 'zod'
const CreateUserSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'Must be 18+'),
})
export async function createUser(formData: FormData) {
const parsed = CreateUserSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
age: Number(formData.get('age')),
})
if (!parsed.success) {
return { errors: parsed.error.flatten().fieldErrors }
}
await db.user.create({ data: parsed.data })
revalidatePath('/users')
return { success: true }
}
Authentication in Server Actions
'use server'
import { auth } from '@/lib/auth'
import { redirect } from 'next/navigation'
export async function deletePost(postId: string) {
const session = await auth()
// Guard: must be logged in
if (!session) redirect('/login')
// Guard: must own the post
const post = await db.post.findById(postId)
if (post.authorId !== session.user.id) {
throw new Error('Unauthorized')
}
await db.post.delete({ where: { id: postId } })
revalidatePath('/blog')
}
Optimistic Updates with useOptimistic
'use client'
import { useOptimistic } from 'react'
import { toggleTodo } from '@/app/actions'
export default function TodoList({ todos }) {
const [optimisticTodos, updateOptimistic] = useOptimistic(
todos,
(state, todoId) =>
state.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
)
return (
<ul>
{optimisticTodos.map(todo => (
<li
key={todo.id}
style={{ textDecoration: todo.done ? 'line-through' : 'none' }}
onClick={async () => {
updateOptimistic(todo.id) // Instant UI update
await toggleTodo(todo.id) // Actual server call
}}
>
{todo.text}
</li>
))}
</ul>
)
}
Conclusion
Server Actions fundamentally simplify Next.js development. Database mutations, form submissions, and server-side logic can now live right alongside your UI — no separate API route, no fetch call, no JSON handling. For full-stack Next.js apps in 2026, Server Actions are the modern, recommended way to handle data mutations.