·Updated

Server Actions vs Route Handlers: When to Use Each in Next.js

Server Actions handle internal mutations with type safety. Route Handlers build external APIs. Learn when to use each in Next.js with a clear decision framework.

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.

CriteriaServer ActionsRoute Handlers
Called fromReact componentsAny HTTP client
HTTP methodsPOST onlyGET, POST, PUT, DELETE, etc.
Type safetyAutomatic (function calls)Manual (you parse requests)
CachingNot cacheableGET requests cacheable
Progressive enhancementYes (forms work without JS)No
Best forInternal mutationsExternal 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.tsx
import { 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.ts
import { 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 directly
data 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:

  1. Validate all input with Zod or similar
  2. Check authentication in every protected function
  3. Verify authorization (not just "is logged in" but "can do this action")
  4. 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, inviteMember

Route 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/:id

Keep 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 Component
export 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.ts
export 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?
Server Actions are functions you call from React components that run on the server. Route Handlers are traditional HTTP endpoints that any client can call. Server Actions are simpler for internal use; Route Handlers are necessary for external access.
Can Server Actions handle GET requests?
No. Server Actions only support POST requests. For GET endpoints, use Route Handlers or fetch data directly in Server Components.
Are Server Actions secure?
Server Actions run on the server, so secrets stay protected. But they're public HTTP endpoints, meaning you must validate input, check authentication, and verify authorization. Treat them like any API endpoint.
Which is better for performance?
For internal mutations, Server Actions are typically faster due to Next.js optimization. For read-heavy APIs serving external clients, Route Handlers with HTTP caching perform better.
Can I use both in the same project?
Yes, and you should. Use Server Actions for internal mutations and Route Handlers for webhooks and external APIs. Most projects use Server Actions for 90%+ of server code.
Do Server Actions work without JavaScript?
Yes, when used with the form action prop. This is called progressive enhancement. The form submits as a standard POST request if JavaScript is disabled.
When should I use a Route Handler instead of a Server Action?
Use Route Handlers when external services need to call your API (webhooks, mobile apps), when you need cacheable GET endpoints, or when you need precise control over HTTP responses.

Next Steps

Now you know when to use each approach:

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.

Some other posts you might like...