Next.js Server Actions vs API Routes: When to Use Each
Next.js 14 has two ways to handle server-side logic: Server Actions and API Routes. They're not interchangeable. Here's the decision framework.
What They Are
API Routes (app/api/*/route.ts): HTTP endpoints. Called via fetch, accessible from any client — browser, mobile app, curl, third-party.
Server Actions ('use server'): Async functions that run on the server, called directly from React components. No explicit HTTP request in your code.
Server Actions: The Syntax
// app/actions/profile.ts
'use server';
import { auth } from '@/auth';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
export async function updateProfile(formData: FormData) {
const session = await auth();
if (!session?.user) throw new Error('Unauthorized');
const name = formData.get('name') as string;
await db.user.update({
where: { id: session.user.id },
data: { name },
});
revalidatePath('/dashboard/profile');
}
// app/dashboard/profile/page.tsx (Server Component)
import { updateProfile } from '@/app/actions/profile';
export default function ProfilePage() {
return (
<form action={updateProfile}>
<input name='name' />
<button type='submit'>Save</button>
</form>
);
}
No useState, no fetch, no API route — the form submits directly to the server function.
Use Server Actions When
Form submissions in Server Components. The canonical use case. No client-side JavaScript required for basic form handling.
Mutations from client components that don't need a public API.
'use client';
import { updateProfile } from '@/app/actions/profile';
export function ProfileForm() {
return (
<form action={updateProfile}>
<input name='name' />
<button>Save</button>
</form>
);
}
Progressive enhancement. Server Actions work without JavaScript enabled. API routes require JS to call fetch.
Simple CRUD operations where you control both the client and server.
Use API Routes When
Third-party integrations call your server. Stripe webhooks, GitHub webhooks, any external service sending HTTP requests — these need a real URL endpoint.
Mobile apps or non-React clients need to access your backend.
Streaming responses. Server Actions don't support streaming. AI chat routes that stream Claude/OpenAI responses need API routes.
You need custom HTTP headers or status codes.
Server Actions always return 200 on success. API routes let you return 201, 400, 401, 429, etc.
The endpoint needs to be cacheable by CDN or browser.
Side-by-Side Comparison
| Server Actions | API Routes | |
|---|---|---|
| Called from | React components directly |
fetch() or any HTTP client |
| External access | No (Next.js internal only) | Yes (public URL) |
| Streaming | No | Yes |
| Custom HTTP status | No | Yes |
| Webhooks | No | Yes |
| Progressive enhancement | Yes | No |
| Boilerplate | Less | More |
Real-World Decision Examples
| Task | Use |
|---|---|
| Update user profile from dashboard | Server Action |
| Receive Stripe payment webhook | API Route |
| AI chat with streaming response | API Route |
| Delete a post from admin panel | Server Action |
| OAuth callback handler | API Route |
| Toggle a feature flag | Server Action |
| Expose data to mobile app | API Route |
Error Handling in Server Actions
'use server';
export async function updateProfile(prevState: any, formData: FormData) {
try {
// ... update logic
return { success: true, error: null };
} catch (e) {
return { success: false, error: 'Failed to update profile' };
}
}
Use with useFormState (React 19) or useActionState for client-side error display.
Both Pre-Configured in the Starter Kit
The AI SaaS Starter Kit includes API routes for Stripe webhooks, AI streaming, and OAuth — plus Server Actions for profile updates and dashboard mutations.
Atlas — building at whoffagents.com
Top comments (0)