14 KiB
Server Actions vs API Routes Decision Matrix
Comprehensive guide for choosing between Server Actions and API routes in Next.js applications.
Quick Decision Tree
Is it a form submission?
├─ YES → Use Server Action (unless external API involved)
└─ NO
├─ Is it a webhook or external callback?
│ └─ YES → Use API Route
└─ NO
├─ Does it need non-POST methods (GET, PUT, DELETE)?
│ └─ YES → Use API Route
└─ NO
├─ Does it proxy an external API?
│ └─ YES → Use API Route
└─ NO
├─ Does it need revalidatePath/revalidateTag?
│ └─ YES → Use Server Action
└─ NO → Either works (prefer Server Action for simplicity)
Detailed Decision Matrix
| Criterion | Server Action | API Route | Notes |
|---|---|---|---|
| Form submission | [OK] Strongly recommended | - | Progressive enhancement, CSRF protection |
| Data mutation | [OK] Recommended | ○ Works | Server Actions simpler for mutations |
| Revalidation needed | [OK] Built-in | - Requires manual | revalidatePath(), revalidateTag() |
| External API proxy | - Not ideal | [OK] Recommended | Hide API keys, rate limiting |
| Webhooks | - Cannot use | [OK] Required | Needs public URL endpoint |
| OAuth callbacks | - Cannot use | [OK] Required | Third-party redirects |
| GET requests | - POST only | [OK] Required | Server Actions are POST-only |
| Multiple HTTP methods | - POST only | [OK] Required | REST API with GET/POST/PUT/DELETE |
| External client access | - Internal only | [OK] Required | Mobile apps, third-party integrations |
| Custom response headers | - Limited | [OK] Full control | CORS, caching, content-type |
| Streaming responses | ○ Possible | [OK] Easier | SSE, custom streams |
| File uploads | [OK] FormData | ○ Works | Server Actions handle FormData natively |
| Type safety | [OK] Full | ○ Manual | Server Actions fully type-safe |
| CSRF protection | [OK] Automatic | - Manual | Server Actions have built-in protection |
| Progressive enhancement | [OK] Works without JS | - Requires JS | Forms work even if JS fails |
| Authentication in RSC | [OK] Direct access | - Via middleware | Server Actions can access auth directly |
Legend: [OK] = Strongly recommended, ○ = Works but not ideal, - = Not suitable/possible
Use Case Patterns
1. Form Submissions
Scenario: User submits a form to create/update data
Recommended: Server Action
Reasoning:
- Built-in form handling with
actionattribute - Automatic CSRF protection
- Progressive enhancement (works without JavaScript)
- Simpler code than fetch + API route
- Type-safe with TypeScript
Example:
// Server Action (recommended)
'use server';
export async function createEntity(formData: FormData) {
const name = formData.get('name') as string;
await db.entity.create({ name });
revalidatePath('/entities');
return { success: true };
}
// Usage in component
<form action={createEntity}>
<input name="name" required />
<button type="submit">Create</button>
</form>
2. External API Proxying
Scenario: Client needs to call external API, but API keys must be hidden
Recommended: API Route
Reasoning:
- Hides API keys on server
- Enables rate limiting
- Allows response transformation
- Can cache responses
- Standard REST pattern
Example:
// API Route (recommended)
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const query = searchParams.get('query');
const response = await fetch(
`https://api.external.com?key=${process.env.API_KEY}&q=${query}`,
{ next: { revalidate: 3600 } } // Cache for 1 hour
);
return Response.json(await response.json());
}
3. Webhooks
Scenario: External service (Stripe, GitHub, etc.) sends POST requests
Recommended: API Route
Reasoning:
- Needs public, stable URL
- External service cannot call Server Actions
- Requires signature verification
- May need custom response codes/headers
Example:
// API Route (required)
export async function POST(request: Request) {
const signature = request.headers.get('stripe-signature');
const body = await request.text();
// Verify webhook signature
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.WEBHOOK_SECRET
);
// Process event
await handleWebhookEvent(event);
return Response.json({ received: true });
}
4. Data Mutations with Revalidation
Scenario: Update data and refresh cache
Recommended: Server Action
Reasoning:
- Built-in
revalidatePath()andrevalidateTag() - Simpler than manual cache invalidation
- Type-safe
- Direct server access
Example:
// Server Action (recommended)
'use server';
export async function updateEntity(id: string, data: any) {
await db.entity.update({ where: { id }, data });
revalidatePath('/entities');
revalidateTag(`entity-${id}`);
return { success: true };
}
5. OAuth Callbacks
Scenario: Third-party service redirects back to your app
Recommended: API Route
Reasoning:
- Needs stable public URL
- External redirect target
- May need to set cookies/headers
- Standard OAuth flow pattern
Example:
// API Route (required)
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
// Exchange code for token
const token = await exchangeCodeForToken(code);
// Set cookie and redirect
const response = NextResponse.redirect('/dashboard');
response.cookies.set('auth_token', token, { httpOnly: true });
return response;
}
6. Public REST API
Scenario: Expose API endpoints for mobile apps or third-party integrations
Recommended: API Route
Reasoning:
- External client access
- Standard REST conventions
- Multiple HTTP methods (GET, POST, PUT, DELETE)
- Custom authentication (API keys, tokens)
- Can document with OpenAPI/Swagger
Example:
// API Route (required)
export async function GET(request: Request) {
const token = request.headers.get('authorization');
if (!validateApiKey(token)) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const entities = await db.entity.findMany();
return Response.json({ entities });
}
7. File Uploads
Scenario: User uploads files
Recommended: Server Action
Reasoning:
- Native FormData handling
- Simpler than API route with multipart parsing
- Can stream large files
- Progressive enhancement
Example:
// Server Action (recommended)
'use server';
export async function uploadFile(formData: FormData) {
const file = formData.get('file') as File;
// Process file
const buffer = Buffer.from(await file.arrayBuffer());
await saveFile(buffer, file.name);
revalidatePath('/files');
return { success: true, filename: file.name };
}
8. Optimistic Updates
Scenario: Update UI immediately, validate on server
Recommended: Server Action
Reasoning:
- Works with
useOptimistichook - Automatic rollback on error
- Type-safe
- Simpler state management
Example:
'use client';
import { useOptimistic } from 'react';
function Component() {
const [optimisticData, addOptimistic] = useOptimistic(data);
async function handleUpdate(newData) {
addOptimistic(newData); // Update UI immediately
await serverAction(newData); // Validate on server
}
return <UI data={optimisticData} onUpdate={handleUpdate} />;
}
9. Server-Sent Events (SSE)
Scenario: Push real-time updates to client
Recommended: API Route
Reasoning:
- Needs streaming response
- Custom headers (Content-Type: text/event-stream)
- Long-lived connection
- Standard SSE protocol
Example:
// API Route (recommended)
export async function GET() {
const stream = new ReadableStream({
start(controller) {
const interval = setInterval(() => {
controller.enqueue(`data: ${JSON.stringify({ time: Date.now() })}\n\n`);
}, 1000);
return () => clearInterval(interval);
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
10. Complex Authentication Flows
Scenario: Multi-step authentication with redirects
Recommended: API Route
Reasoning:
- Full control over response
- Custom headers and cookies
- Complex redirect logic
- Standard auth patterns
Example:
// API Route (recommended)
export async function POST(request: Request) {
const { email, password } = await request.json();
const user = await authenticateUser(email, password);
if (!user) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 });
}
const response = NextResponse.json({ user });
response.cookies.set('session', user.sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 7, // 7 days
});
return response;
}
Performance Considerations
Server Actions
Pros:
- Automatic request deduplication
- Built-in caching with React
- No additional route handler overhead
- Smaller client bundle (no fetch logic)
Cons:
- POST-only (cannot use GET with browser caching)
- No HTTP-level caching (no Cache-Control headers)
API Routes
Pros:
- Full HTTP caching support (Cache-Control, ETag)
- GET requests can leverage browser cache
- Can use CDN caching
- Standard HTTP optimizations
Cons:
- Additional route handler overhead
- Requires client-side fetch logic
- No automatic deduplication
Security Considerations
Server Actions
Pros:
- Built-in CSRF protection
- No CORS issues (same-origin)
- Automatic request validation
- Type-safe by default
Cons:
- Cannot restrict by IP or API key
- Limited rate limiting options
API Routes
Pros:
- Full control over authentication
- API key/token validation
- IP-based restrictions
- Custom rate limiting
- CORS configuration
Cons:
- Must implement CSRF protection manually
- More attack surface (public endpoints)
Developer Experience
Server Actions
Pros:
- Simpler code (no fetch boilerplate)
- Fully type-safe
- Better error handling with React
- Progressive enhancement
- Automatic form reset
Cons:
- Less familiar to backend developers
- Debugging can be harder (no network tab)
- Limited to Next.js ecosystem
API Routes
Pros:
- Standard REST patterns
- Familiar to all developers
- Easy to test (curl, Postman)
- Visible in network tab
- Framework-agnostic clients
Cons:
- More boilerplate code
- Manual type safety
- Requires client-side error handling
- CSRF protection needed
Migration Strategies
API Route → Server Action
When to migrate:
- POST-only endpoint
- Used for form submissions
- Needs revalidation
- Only called from your app
Steps:
- Create Server Action with same logic
- Remove fetch call from client
- Update form to use
actionprop - Add revalidation if needed
- Remove API route
- Test thoroughly
Complexity: Low to Medium
Server Action → API Route
When to migrate:
- Need external client access
- Require GET/PUT/DELETE methods
- Need custom headers/cookies
- Webhook target
Steps:
- Create API route with same logic
- Add authentication/authorization
- Remove revalidation (use cache instead)
- Update clients to use fetch
- Remove Server Action
- Test with external clients
Complexity: Medium to High
Worldbuilding App Examples
Entity Management
| Operation | Recommended | Reason |
|---|---|---|
| Create entity form | Server Action | Form submission, revalidation |
| Update entity form | Server Action | Form submission, revalidation |
| Delete entity | Server Action | Simple mutation, revalidation |
| Get entity list | API Route (GET) | External dashboard, caching |
| Bulk import | Server Action | FormData, file upload |
| Export entities | API Route (GET) | Streaming, custom headers |
Relationships
| Operation | Recommended | Reason |
|---|---|---|
| Add relationship | Server Action | Simple mutation, revalidation |
| Remove relationship | Server Action | Simple mutation, revalidation |
| Get relationship graph | API Route (GET) | Complex data, caching |
| Bulk relationship update | Server Action | Transaction, revalidation |
Timeline Events
| Operation | Recommended | Reason |
|---|---|---|
| Create event | Server Action | Form submission, revalidation |
| Update event | Server Action | Form submission, revalidation |
| Timeline feed | API Route (GET) | Caching, pagination |
| Event search | API Route (GET) | Complex query, caching |
Search and Filtering
| Operation | Recommended | Reason |
|---|---|---|
| Search within app | Server Action | With revalidation, type-safe |
| Public search API | API Route (GET) | External access, rate limiting |
| Save search filter | Server Action | Form submission, revalidation |
Best Practices Summary
- Default to Server Actions for forms - Simpler, more secure, better UX
- Use API Routes for external integration - Webhooks, proxies, third-party APIs
- Consider HTTP methods - Non-POST operations need API routes
- Think about clients - External clients require API routes
- Evaluate caching needs - Complex caching may favor API routes
- Prioritize type safety - Server Actions provide better type safety
- Plan for scale - API routes better for public APIs with rate limiting
- Progressive enhancement - Forms with Server Actions work without JS
- Monitor performance - Both have trade-offs, measure and optimize
- Be consistent - Choose patterns and stick to them across your codebase