// LOADING
// LOADING
// LOADING_ARTICLE
Next.js gives you two server-side execution models: Route Handlers and Server Actions. They overlap in capability but diverge sharply in design intent. Picking the wrong one for a job creates unnecessary complexity; picking the right one makes your code simpler.
Route Handlers are Next.js's equivalent of API endpoints. You export named HTTP method functions (GET, POST, PUT, DELETE, etc.) from files at app/api/**/route.ts:
ts// app/api/posts/route.ts import { NextResponse } from 'next/server'; export async function GET() { const posts = await db.post.findMany(); return NextResponse.json(posts); } export async function POST(request: Request) { const body = await request.json(); const post = await db.post.create({ data: body }); return NextResponse.json(post, { status: 201 }); }
Route Handlers run on the server, return standard Response objects, and are consumed over HTTP by any client — a browser, a mobile app, a third-party service, or your own frontend. They are the right choice when you need an actual HTTP API.
Server Actions are async functions that run on the server but are called from client or server components directly — no fetch required. You mark them with 'use server':
ts// app/actions/posts.ts 'use server'; import { revalidatePath } from 'next/cache'; export async function createPost(formData: FormData) { const title = formData.get('title') as string; await db.post.create({ data: { title } }); revalidatePath('/posts'); }
They can be called from a <form action={createPost}> or invoked programmatically from a Client Component with startTransition. React handles the network round-trip; you just call a function.
The key question is: who consumes this server-side logic?
Cache-Control, CDN)? Route Handler.Server Actions shine for mutations tied to your UI. Route Handlers shine for public-facing or externally consumed APIs. This distinction is central to how to design APIs for Next.js applications.
One underused feature of Server Actions is that they work without JavaScript. Because they hook directly into HTML <form> submissions, a form backed by a Server Action degrades gracefully:
tsx// This form works even with JS disabled export default function NewPostForm() { return ( <form action={createPost}> <input name="title" required /> <button type="submit">Create</button> </form> ); }
This is progressive enhancement in practice — a concept that pairs naturally with the resilience patterns in error boundaries and graceful failure in React.
Server Actions integrate natively with Next.js caching. After a mutation, call revalidatePath or revalidateTag inside the action to purge stale cached data:
ts'use server'; import { revalidateTag } from 'next/cache'; export async function deletePost(id: string) { await db.post.delete({ where: { id } }); revalidateTag('posts'); }
With a Route Handler, you'd need to either trigger revalidation via an on-demand API call or rely on time-based ISR. Server Actions make the mutation-revalidation loop tighter and more explicit. For a broader look at caching strategies including ISR, see caching strategies: CDN, ISR, Redis.
Both approaches need input validation. With Server Actions the pattern is to return error state rather than throw:
ts'use server'; import { z } from 'zod'; const schema = z.object({ title: z.string().min(1).max(200), }); export async function createPost(formData: FormData) { const result = schema.safeParse({ title: formData.get('title') }); if (!result.success) { return { error: result.error.flatten() }; } await db.post.create({ data: result.data }); return { success: true }; }
With Route Handlers, return a structured JSON error response with an appropriate HTTP status. Runtime validation with Zod — covered in detail in runtime validation in TypeScript with Zod — works cleanly in both models.
Route Handlers read auth from request cookies or headers. Server Actions can use the same session utilities because they run in the same server environment:
ts// Route Handler export async function GET(request: Request) { const session = await getSession(request); if (!session) return new Response('Unauthorized', { status: 401 }); // ... } // Server Action 'use server'; export async function sensitiveAction() { const session = await getServerSession(); if (!session) throw new Error('Unauthorized'); // ... }
Middleware handles the first line of defense for auth gating. Read more about that layer in Next.js middleware: auth, redirects, and edge logic.
For mutations that are only ever called from your own app, either approach technically works. The practical tiebreaker:
Route Handlers and Server Actions are not competing features — they solve different parts of the server-communication problem. Build public APIs with Route Handlers. Wire mutations in your own app with Server Actions. Understanding where the line falls, and why, is the kind of architectural clarity that becomes second nature as you work on larger Next.js projects — see the projects section for examples of these patterns in real codebases. This topic also comes up frequently in interview prep for senior Next.js roles.