# 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 `action` attribute - Automatic CSRF protection - Progressive enhancement (works without JavaScript) - Simpler code than fetch + API route - Type-safe with TypeScript **Example**: ```typescript // 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
``` ### 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**: ```typescript // 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**: ```typescript // 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()` and `revalidateTag()` - Simpler than manual cache invalidation - Type-safe - Direct server access **Example**: ```typescript // 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**: ```typescript // 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**: ```typescript // 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**: ```typescript // 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 `useOptimistic` hook - Automatic rollback on error - Type-safe - Simpler state management **Example**: ```typescript '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