Next.js Cheatsheet
Server Actions
Use this Next.js reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
What Are Server Actions?
Server Actions are async functions that run on the server, callable from Client Components (via forms or event handlers) or Server Components. They replace manual API route boilerplate for mutations.
Defining Server Actions
Option 1 — Inline in a Server Component (single action):
// app/contact/page.tsx (Server Component) export default function ContactPage() { async function sendMessage(formData: FormData) { 'use server' // directive inside the function const message = formData.get('message') as string await db.messages.create({ data: { message } }) } return ( <form action={sendMessage}> <input name="message" /> <button type="submit">Send</button> </form> ) }
Option 2 — Dedicated actions file (reusable across components):
// app/actions.ts (or lib/actions.ts) 'use server' // directive at top of file export async function createPost(formData: FormData) { const title = formData.get('title') as string await db.posts.create({ data: { title } }) } export async function deletePost(id: string) { await db.posts.delete({ where: { id } }) }
'use server'at the top of a file marks every export in that file as a Server Action.
Using Actions in a <form>
// Server Component — no JS needed on the client import { createPost } from '@/app/actions' export default function NewPostForm() { return ( <form action={createPost}> <input name="title" placeholder="Post title" required /> <button type="submit">Create</button> </form> ) }
Calling Actions from Client Components
'use client' import { createPost } from '@/app/actions' export function PostForm() { return ( <form action={createPost}> <input name="title" /> <button type="submit">Create</button> </form> ) }
Via event handler (not a form):
'use client' import { deletePost } from '@/app/actions' export function DeleteButton({ id }: { id: string }) { return ( <button onClick={() => deletePost(id)}> Delete </button> ) }
useActionState — Pending State + Return Value
'use client' import { useActionState } from 'react' import { createPost } from '@/app/actions' const initialState = { message: '', error: '' } export function CreatePostForm() { const [state, formAction, isPending] = useActionState(createPost, initialState) return ( <form action={formAction}> <input name="title" /> <button type="submit" disabled={isPending}> {isPending ? 'Creating…' : 'Create'} </button> {state.message && <p>{state.message}</p>} {state.error && <p className="text-red-500">{state.error}</p>} </form> ) }
// app/actions.ts 'use server' type State = { message: string; error: string } export async function createPost(prevState: State, formData: FormData): Promise<State> { const title = formData.get('title') as string if (!title) return { message: '', error: 'Title is required' } await db.posts.create({ data: { title } }) return { message: 'Post created!', error: '' } }
useActionStatewas previously calleduseFormState(React 18 / Next 14). UseuseActionStatein React 19 / Next 15+.
startTransition — Programmatic Trigger
'use client' import { useTransition } from 'react' import { updateUser } from '@/app/actions' export function EditProfile({ user }: { user: User }) { const [isPending, startTransition] = useTransition() function handleSave() { startTransition(async () => { await updateUser({ id: user.id, name: 'New Name' }) }) } return ( <button onClick={handleSave} disabled={isPending}> {isPending ? 'Saving…' : 'Save'} </button> ) }
Passing Extra Arguments with .bind()
// app/actions.ts 'use server' export async function deletePost(id: string, formData: FormData) { await db.posts.delete({ where: { id } }) }
// component import { deletePost } from '@/app/actions' export function PostItem({ post }: { post: Post }) { const deleteWithId = deletePost.bind(null, post.id) return ( <form action={deleteWithId}> <button type="submit">Delete</button> </form> ) }
Revalidating After Mutations
'use server' import { revalidatePath, revalidateTag } from 'next/cache' import { redirect } from 'next/navigation' export async function createPost(formData: FormData) { const post = await db.posts.create({ data: { title: formData.get('title') as string } }) revalidatePath('/posts') // re-render /posts revalidateTag('posts') // invalidate tagged fetches redirect(`/posts/${post.id}`) // navigate after success }
redirect()inside a Server Action throws internally — noreturnneeded.
Error Handling
'use server' export async function updateProfile(formData: FormData) { try { await db.users.update({ where: { id: formData.get('id') as string }, data: { name: formData.get('name') as string } }) revalidatePath('/profile') } catch (err) { // Return error state (works with useActionState) return { error: 'Failed to update profile' } } }
For unexpected errors, use error.tsx — unhandled throws are caught by the error boundary.
Security Considerations
| Concern | Details |
|---|---|
| Authentication | Always check auth inside the action — anyone can call an exposed action |
| CSRF | Next.js adds automatic CSRF protection for same-origin form submissions |
| Input validation | Validate/sanitize all formData inputs (use Zod or similar) |
| Rate limiting | Apply in-memory or Redis rate limits for expensive operations |
'use server' import { z } from 'zod' import { getCurrentUser } from '@/lib/auth' const schema = z.object({ title: z.string().min(1).max(200) }) export async function createPost(formData: FormData) { const user = await getCurrentUser() if (!user) throw new Error('Unauthorized') const parsed = schema.safeParse({ title: formData.get('title') }) if (!parsed.success) return { error: parsed.error.flatten() } await db.posts.create({ data: { title: parsed.data.title, userId: user.id } }) revalidatePath('/posts') }
Optimistic Updates with useOptimistic
'use client' import { useOptimistic, useTransition } from 'react' import { toggleLike } from '@/app/actions' export function LikeButton({ post }: { post: Post }) { const [optimisticLiked, setOptimisticLiked] = useOptimistic(post.liked) const [, startTransition] = useTransition() function handleClick() { startTransition(async () => { setOptimisticLiked(prev => !prev) // instant UI update await toggleLike(post.id) // actual server call }) } return ( <button onClick={handleClick}> {optimisticLiked ? '♥ Liked' : '♡ Like'} </button> ) }