Next.js gives you two ways to run server-side code: Server Actions and Route Handlers. They solve different problems, but the overlap causes confusion.
The short answer: Use Server Actions for mutations called from your React components. Use Route Handlers when external clients need to call your API.
| Criteria | Server Actions | Route Handlers |
|---|---|---|
| Called from | React components | Any HTTP client |
| HTTP methods | POST only | GET, POST, PUT, DELETE, etc. |
| Type safety | Automatic (function calls) | Manual (you parse requests) |
| Caching | Not cacheable | GET requests cacheable |
| Progressive enhancement | Yes (forms work without JS) | No |
| Best for | Internal mutations | External APIs, webhooks |
Tested with Next.js 16.1 and React 19 in January 2026. Both features are stable and production-ready.
What Are Server Actions?
Server Actions are async functions that run on the server but you call them like regular functions from React components. Next.js handles the HTTP request behind the scenes.
// app/actions.ts'use server';import { revalidatePath } from 'next/cache';export async function createPost(formData: FormData) { const title = formData.get('title') as string; await db.posts.create({ data: { title } }); revalidatePath('/posts');}// app/posts/new/page.tsximport { createPost } from '@/app/actions';export default function NewPostPage() { return ( <form action={createPost}> <input name="title" required /> <button type="submit">Create</button> </form> );}No fetch calls. No request/response handling. Just a function that happens to run on the server.
For the complete guide, see Next.js Server Actions: The Complete Guide.
What Are Route Handlers?
Route Handlers are traditional API endpoints. You export HTTP method functions from route.ts files and work with standard Request and Response objects.
// app/api/posts/route.tsimport { NextRequest, NextResponse } from 'next/server';export async function GET() { const posts = await db.posts.findMany(); return NextResponse.json(posts);}export async function POST(request: NextRequest) { const { title } = await request.json(); const post = await db.posts.create({ data: { title } }); return NextResponse.json(post, { status: 201 });}Route Handlers give you full control over HTTP: status codes, headers, streaming responses, caching directives.
For the complete guide, see Next.js Route Handlers: The Complete Guide.
When to Use Server Actions
Use Server Actions when:
- You're handling form submissions from your Next.js app
- You're running mutations (create, update, delete) triggered by UI interactions
- You want automatic TypeScript types between client and server
- You need progressive enhancement (forms that work without JavaScript)
- The caller is always your own React code
How MakerKit uses Server Actions:
- Creating a new organization
- Updating user profile settings
- Deleting a team member
- Submitting a contact form
- Toggling feature flags
Server Actions are the better fit here because you get type safety, simpler code, and built-in integration with React's form handling. In MakerKit's SaaS kits, we use dozens of Server Actions for team management, profile updates, and feature toggles. Route Handlers? Just a handful for webhooks.
Avoid Server Actions when:
- External services need to call your endpoint (they can't)
- You need HTTP caching (Server Actions are POST-only)
- You're building a public API for third parties
When to Use Route Handlers
Use Route Handlers when:
- External services need to call your API (webhooks, mobile apps, third parties)
- You're building a public API that others will consume
- You need GET endpoints that benefit from HTTP caching
- You need fine-grained control over HTTP responses (streaming, custom headers)
- You're proxying requests to external services
How MakerKit uses Route Handlers:
- Stripe webhook endpoint (
/api/webhooks/stripe) - Lemon Squeezy webhook endpoint
- Public API for mobile app consumption
- OAuth callback handlers
- Health check endpoints for monitoring
Route Handlers are the only option when the caller isn't your React app. Webhooks, mobile apps, and third-party integrations can't call Server Actions directly.
Avoid Route Handlers when:
- Only your React components call the endpoint (use Server Actions instead)
- You want type safety without extra work
- You need progressive enhancement for forms
The Decision Framework
Ask these questions in order:
1. Is the caller external to your Next.js app?
- Yes → Route Handler
- No → Continue
2. Is it a read operation (GET)?
- Yes, and needs HTTP caching → Route Handler
- Yes, called from Server Component → Just fetch in the component
- No → Continue
3. Is it a mutation triggered from UI?
- Yes → Server Action
Our rule: if only your Next.js app calls it, use Server Actions. Refactor to Route Handler only when you hit the limitations listed above.
External caller? ─────── Yes ──────► Route Handler │ No │ ▼GET with caching? ────── Yes ──────► Route Handler │ No │ ▼UI mutation? ─────────── Yes ──────► Server Action │ No │ ▼Server Component ──────────────────► Fetch directlydata fetch?Can I Use Both?
Yes, and you should. They complement each other.
In MakerKit, we use:
- Server Actions for all internal mutations (95% of server code)
- Route Handlers for webhooks and the few cases where external clients need access
We learned this split the hard way. Early versions of MakerKit used Route Handlers for everything, including internal form submissions. The code worked, but we lost progressive enhancement and had to manually wire up types between client and server. Refactoring to Server Actions cut our mutation code nearly in half.
Don't force one approach where the other fits better.
Performance Comparison
Server Actions:
- Single roundtrip that can return updated UI
- POST-only, so no browser caching
- Colocated with components, no network waterfall for setup
Route Handlers:
- GET responses can be cached at CDN edge
- Standard HTTP semantics allow browser caching
- Slightly more overhead for simple mutations (you write more code)
For mutations called from your app, Server Actions are typically faster because Next.js optimizes the request/response cycle. For read-heavy public APIs, Route Handlers with proper caching win.
Security Considerations
Both require the same security practices:
- Validate all input with Zod or similar
- Check authentication in every protected function
- Verify authorization (not just "is logged in" but "can do this action")
- Rate limit sensitive operations
Server Actions look like regular functions, which can create a false sense of security. They're still public HTTP endpoints. Here's what proper authentication looks like:
'use server';import { getSession } from '@/lib/auth';export async function deletePost(postId: string) { const session = await getSession(); if (!session?.user) { throw new Error('Not authenticated'); } const post = await db.posts.findUnique({ where: { id: postId } }); if (post?.authorId !== session.user.id) { throw new Error('Not authorized'); } await db.posts.delete({ where: { id: postId } });}For the full security checklist, see Server Actions Security: 5 Vulnerabilities You Must Fix.
Code Organization
Server Actions work best in dedicated files:
app/├── actions/│ ├── posts.ts # createPost, updatePost, deletePost│ ├── users.ts # updateProfile, deleteAccount│ └── teams.ts # createTeam, inviteMemberRoute Handlers follow URL structure:
app/├── api/│ ├── webhooks/│ │ ├── stripe/route.ts│ │ └── lemon-squeezy/route.ts│ └── v1/│ └── posts/│ ├── route.ts # GET /api/v1/posts│ └── [id]/route.ts # GET /api/v1/posts/:idKeep them separate. Mixing Server Actions into api/ routes creates confusion.
Common Mistakes
1. Using Server Actions for data fetching
Server Actions use POST and can't be cached. Fetch data in Server Components or use Route Handlers with GET.
// Wrong'use server';export async function getPosts() { return db.posts.findMany();}// Right - fetch in Server Componentexport default async function PostsPage() { const posts = await db.posts.findMany(); return <PostList posts={posts} />;}2. Creating Route Handlers for internal mutations
If only your React components call the endpoint, Server Actions are simpler:
// Unnecessary Route Handler// app/api/posts/route.tsexport async function POST(request: NextRequest) { const { title } = await request.json(); await db.posts.create({ data: { title } }); return NextResponse.json({ success: true });}// Simpler Server Action// app/actions/posts.ts'use server';export async function createPost(formData: FormData) { const title = formData.get('title') as string; await db.posts.create({ data: { title } });}3. Forgetting that Server Actions are public endpoints
The 'use server' directive doesn't add authentication. Validate, authenticate, and authorize in every action.
Quick Recommendation
Server Actions are best for:
- Form submissions
- Button-triggered mutations
- Any UI-driven state change
- Teams that want simpler code with type safety
Route Handlers are best for:
- Webhook endpoints
- Public APIs
- Cacheable GET endpoints
- Mobile app backends
Our pick: Default to Server Actions for internal mutations. Add Route Handlers only when you need external access or HTTP caching. This keeps most of your code simple while handling edge cases properly.
Frequently Asked Questions
What's the main difference between Server Actions and Route Handlers?
Can Server Actions handle GET requests?
Are Server Actions secure?
Which is better for performance?
Can I use both in the same project?
Do Server Actions work without JavaScript?
When should I use a Route Handler instead of a Server Action?
Next Steps
Now you know when to use each approach:
- Server Actions for internal mutations → Complete Server Actions Guide
- Route Handlers for external APIs → Complete Route Handlers Guide
- Securing either approach → Server Actions Security Guide
MakerKit uses this hybrid pattern across all kits. Server Actions handle the bulk of mutations while Route Handlers manage webhooks and integrations. Check out our open-source starter to see these patterns in action.