When building applications with Next.js 13+ (App Router), you have two main ways to handle server-side logic:
Server Actions: Functions that run on the server, called directly from your components
API Routes: Traditional HTTP endpoints that handle requests
Both serve different purposes, and knowing when to use each will help you write better code.
What's the Main Difference?
Server Actions: Think of these as functions you can call directly from your React components. They're great for form submissions and mutations.
API Routes: These are like traditional backend endpoints. External services can call them, and they’re perfect for webhooks or third-party integrations.
Server Actions are newer and more tightly integrated with React. API Routes are more traditional and flexible for external communication.
What Are Server Actions?
Server Actions are asynchronous functions that run on the server. They're marked with the 'use server' directive and can be called directly from Client or Server Components.
// app/actions.js
'use server';
export async function createUser(formData) {
const name = formData.get('name');
const email = formData.get('email');
// Save to database
await db.users.create({ name, email });
return { success: true };
}
In the above code, we have created a createUser server action and we can use it in the client component as shown below:
// app/signup/page.js
import { createUser } from '../actions';
export default function SignupPage() {
return (
<form action={createUser}>
<input name="name" placeholder="Your name" />
<input name="email" placeholder="Your email" />
<button type="submit">Sign Up</button>
</form>
);
}
When to Use Server Actions
Form submissions: Perfect for handling form data
Data mutations: Creating, updating, or deleting data
Direct component actions: When a component needs to trigger server logic
Simple database operations: CRUD operations from your UI
Revalidating cache: When you need to refresh cached data after mutations
Server Actions - Pros & Cons
Pros
No need to create API endpoints
Type-safe with TypeScript
Works seamlessly with forms
Progressive enhancement (works without JS)
Less boilerplate code
Cons
Only accessible from your Next.js app
Can't be called by external services
Not RESTful (no standard HTTP methods)
Requires Next.js 13+ App Router
Advanced Server Action Example
// app/actions.js
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function createPost(formData) {
const title = formData.get('title');
const content = formData.get('content');
// Validate data
if (!title || !content) {
return { error: 'All fields required' };
}
// Save to database
const post = await db.posts.create({ title, content });
// Revalidate the posts page to show new post
revalidatePath('/posts');
// Redirect to the new post page
redirect(`/posts/${post.id}`);
}
Server Actions automatically handle CSRF protection and don't expose sensitive logic to the client!
What Are API Routes?
API Routes are server-side endpoints that handle HTTP requests. They're files in the app/api directory that export functions to handle different HTTP methods.
// app/api/users/route.js
import { NextResponse } from 'next/server';
// Handle GET requests
export async function GET(request) {
const users = await db.users.findMany();
return NextResponse.json(users);
}
// Handle POST requests
export async function POST(request) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json(user, { status: 201 });
}
In the above code, we have created HTTP Get and Post method API Routes(Route handlers).
Calling an API Route from the Frontend
// app/users/page.js
'use client'
export default function UsersPage() {
const handleSubmit = async (e) => {
e.preventDefault();
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'John',
email: 'john@example.com'
})
});
const user = await response.json();
console.log('Created user:', user);
};
return <button onClick={handleSubmit}>Create User</button>;
}
When to Use API Routes
Webhooks: Receiving data from external services (Stripe, GitHub, etc.)
Third-party integrations: When external apps need to access your data
Mobile apps: Serving data to iOS/Android applications
Public APIs: Creating endpoints others can consume
Complex authentication flows: OAuth callbacks, custom auth logic
File uploads: Handling multipart form data
Streaming responses: Server-sent events or large data transfers
Custom HTTP headers: When you need fine control over responses
⚠️ Important: External services like Stripe, GitHub webhooks, or mobile apps CANNOT call Server Actions. They need API Routes!
API Routes — Pros & Cons
Pros
Can be called by anyone
Standard HTTP methods (GET, POST, PUT, PATCH, DELETE etc.)
Works with external services
Full control over request/response
Can set custom headers
Great for webhooks
Well-documented pattern
Cons
More boilerplate code
Need to manually handle validation
Requires fetch() calls from frontend
Need to manage CORS for external access
More files to maintain
Separate type definitions needed
Advanced API Route Example
// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
const post = await db.posts.findUnique({
where: { id: params.id }
});
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
);
}
return NextResponse.json(post);
}
export async function DELETE(request, { params }) {
await db.posts.delete({
where: { id: params.id }
});
return NextResponse.json({ success: true });
}
Quick Decision Guide
Use Server Actions When:
Handling form submissions
Mutating data from your UI components
You want less boilerplate code
You need progressive enhancement
You're working within your Next.js app only
You want automatic type safety
Use API Routes When:
Building webhooks for external services
Creating a public API
Supporting mobile apps
You need RESTful endpoints
Handling file uploads
You need custom HTTP headers/status codes
OAuth or complex authentication flows
Can You Use Both? Absolutely! Many apps use Server Actions for form handling and UI mutations, while using API Routes for external integrations and webhooks.
Real-World Example: E-commerce App
Server Action: Adding items to cart from product page
Server Action: Submitting checkout form
API Route: Stripe webhook for payment confirmation
API Route: Mobile app endpoint to fetch products
🔒 Security Tip: Server Actions have built-in CSRF protection. For API Routes accessed externally, implement your own authentication/protection.
Common Mistakes To Avoid
Mistake 1: Using API Routes for Internal Forms
// DON'T do this - unnecessarily complex!
const handleSubmit = async () => {
await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
});
};
// DO this instead - simpler and better!
import { createUser } from './actions';
<form action={createUser}>
<input name="name" />
<button>Submit</button>
</form>
Mistake 2: Trying to Call Server Actions from External Services
Server Actions cannot be called via HTTP requests from outside your app. If you need external access, use API Routes.
Mistake 3: Not Using revalidatePath() After Mutations
// DON'T forget to revalidate!
export async function updatePost(id, data) {
await db.posts.update({
where: { id },
data
});
// Page will show stale/old data!
}
// DO revalidate to show fresh data!
import { revalidatePath } from 'next/cache';
export async function updatePost(id, data) {
await db.posts.update({
where: { id },
data
});
revalidatePath('/posts');
}
Advanced Examples
Example 1: Server Action with Client-Side State
// app/components/AddTodoForm.js
'use client'
import { useFormState, useFormStatus } from 'react-dom';
import { addTodo } from '../actions';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? 'Adding...' : 'Add Todo'}
</button>
);
}
export default function AddTodoForm() {
const [state, formAction] = useFormState(addTodo, null);
return (
<form action={formAction}>
<input name="title" required />
{state?.error && <p>{state.error}</p>}
<SubmitButton />
</form>
);
}
Example 2: API Route with Authentication
// app/api/admin/users/route.js
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
export async function GET(request) {
// Check if user is authenticated
const session = await getServerSession();
if (!session || session.user.role !== 'admin') {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
const users = await db.users.findMany();
return NextResponse.json(users);
}
Resources for Learning More
Final Tip: When in doubt, ask yourself: "Will anything outside my Next.js app need to call this?" If yes → API Route. If no → Server Action.
If you found this article useful, feel free to give claps👏 and share your views in the comment.
Top comments (0)