Server Actions
'use server' directive that runs on the server but can be called directly from your components as if it were a normal JavaScript function — no manual API route, no fetch call, no request/response plumbing. If you haven't seen the 'use server' directive yet, the "The 'use server' Directive" page covers it in isolation; this page focuses on what Server Actions let you build.The old way: a Route Handler plus a fetch call
fetch call on the client that knew that route's URL, method, and body shape.app/api/todos/route.ts — the old way, part 1
import { NextResponse } from 'next/server'
export async function POST(request: Request) {
const body = await request.json()
// ...save body.title to the database...
return NextResponse.json({ success: true })
}components/AddTodo.tsx — the old way, part 2
'use client'
import { useState } from 'react'
export default function AddTodo() {
const [title, setTitle] = useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title }),
})
setTitle('')
}
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Add</button>
</form>
)
}That works, but it's a lot of ceremony for "save this string." You own the URL, the HTTP method, the JSON serialization on both ends, and keeping the client's idea of the request shape in sync with the server's idea of it forever.
The new way: call the function directly
'use server', and call it from a Client Component exactly like a local function — Next.js handles serializing the arguments, sending the request, and returning the result.app/actions/todos.ts
'use server'
export async function addTodo(title: string) {
// ...save title to the database...
return { success: true }
}components/AddTodo.tsx
'use client'
import { useState } from 'react'
import { addTodo } from '@/app/actions/todos'
export default function AddTodo() {
const [title, setTitle] = useState('')
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
await addTodo(title)
setTitle('')
}
return (
<form onSubmit={handleSubmit}>
<input value={title} onChange={(e) => setTitle(e.target.value)} />
<button type="submit">Add</button>
</form>
)
}Inline in a Server Component, or in a shared file
app/todos/page.tsx — inline action in a Server Component
export default function TodosPage() {
async function addTodo(formData: FormData) {
'use server'
const title = formData.get('title')
// ...save title to the database...
}
return (
<form action={addTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
)
}'use server' directive at the top, as in the app/actions/todos.ts example above. Every export in that file becomes an importable Server Action.Placement | Best for |
|---|---|
Inline, inside a Server Component | A single form defined and used in one place; keeps logic colocated |
Separate | Reused across multiple Client Components, or shared by a form and a button |
A Server Action is an async function marked
'use server'that runs on the server but is called like a normal function from your components.It replaces the old pattern of hand-writing a Route Handler plus a matching
fetchcall for simple mutations.Next.js generates a secure, non-guessable endpoint for each action automatically — you never design or expose that API surface yourself.
Define inline inside a Server Component for a form used in one place; move to a separate
'use server'file to reuse across Client Components.