
Hemant Bhatt
The Next.js Backend Handbook: Understanding Server-Side Patterns

Introduction
Next.js offers you several ways to handle server-side logic. Each one has it's own purpose, and mixing them up is like using a hammer to eat soup—messy and embarrassing.
Let's explore how to leverage each way effectively.
The Four Players in Next.js Backend
Before we dive deep, let's meet our cast:
- Server-Only Modules - The secret sauce only the server knows about
- Server Components - Your HTML generators that fetch data
- Server Actions - Your form handlers and mutation specialists
- API Routes - Your traditional REST endpoints
Key insight:
Server-Only Modules are the foundation. The other three are the interfaces.1. Server-Only Modules: The Foundation
Let's start with the most important piece that isn't talked about enough.
Modules: What Are They?
Server-Only Modules are regular TypeScript/JavaScript files that can only run on the server. They're marked with the "server-only"
package to prevent them from accidentally bundling into your client code.
Server-Only Modules can be imported and used by all three server-side patterns—Server Components, Server Actions, and API Routes. They're your shared business logic layer. Create common modules with frequently used server-side logic, and invoke them wherever you need on the server. One module, multiple entry points.
1 import "server-only"; 2 3 export async function getSecretData() { 4 // Direct database access 5 const data = await db.query("SELECT * FROM secrets"); 6 return data; 7 }
Why Use Them?
Two solid reasons:
- Security: Keep your database credentials, API keys, and sensitive logic away from the client bundle
- Organization: Centralize your business logic in one place
The Golden Rule:
If you're touching a database, calling a sensitive API, or using environment variables that start with anything other than 'NEXT_PUBLIC_', it surely, surely(a few more surely) belongs in a server-only module.2. Server Components: The HTML Generators
Server Components are React components that render exclusively on the server. They're the default in the App Router.
What They Do
They fetch data and generate HTML. That's it. Clean, simple, effective.
First, create a server-only module with your database logic:
1 import "server-only"; 2 3 export async function getUserProfile(id: string) { 4 return await db.user.findUnique({ 5 where: { id }, 6 select: { 7 name: true, 8 bio: true, 9 email: true 10 } 11 }); 12 }
Then, use it directly in your Server Component:
1 import { getUserProfile } from "@/lib/server-only/database"; 2 3 export default async function ProfilePage({ 4 params, 5 }: { 6 params: { id: string }; 7 }) { 8 // This runs on the server 9 const user = await getUserProfile(params.id); 10 11 return ( 12 <div> 13 <h1>{user.name}</h1> 14 <p>{user.bio}</p> 15 <p>{user.email}</p> 16 </div> 17 ); 18 }
The Beautiful Part
Server Components can directly import and use server-only modules. No ceremony, no extra steps, no API routes needed. Just import and use.
When to Use Them
- •Displaying data that's available at render time
- •Building static or dynamic pages that don't need interactivity
- •SEO-critical content (it's already HTML!)
When NOT to Use Them
- •Handling form submissions (use Server Actions)
- •Responding to button clicks (use Client Components)
- •Creating REST APIs for external consumption (use API Routes)
3. Server Actions: The Mutation Specialists
Server Actions are functions that run on the server but can be called from the client. They're the modern replacement for API routes when you're handling forms and mutations within your Next.js app.
What Do They Do
They handle mutations: creating, updating, and deleting data. They run on the server but can be triggered from client components, giving you the best of both worlds—server-side security with client-side interactivity.
Here's what a Server Action looks like:
1 "use server"; 2 3 import { revalidatePath } from "next/cache"; 4 import { updateUserProfile } from "@/lib/server-only/database"; 5 6 export async function saveProfile(formData: FormData) { 7 const name = formData.get("name") as string; 8 const bio = formData.get("bio") as string; 9 10 // Call your server-only module 11 await updateUserProfile({ name, bio }); 12 13 // Revalidate the cache 14 revalidatePath("/profile"); 15 16 return { success: true }; 17 }
Calling Them from Client Components
The magic happens when you call them from the client. You can directly import server actions into client components and assign them into event handlers like 'onClick', 'onBlur' and form actions.
1 "use client"; 2 3 import { saveProfile } from "@/actions/profile"; 4 5 export function ProfileForm() { 6 return ( 7 <form action={saveProfile}> 8 <input name="name" /> 9 <input name="bio" /> 10 <button type="submit">Save</button> 11 </form> 12 ); 13 }
Next.js automatically creates an endpoint for your Server Action. You don't see it, but it's there, handling the serialization and security for you.
When to Use Them
- •Form submissions
- •Database mutations (create, update, delete)
- •Actions triggered by user interactions
- •Revalidating cached data
The Beautiful Part
No API route boilerplate. No manual fetch calls. No request/response parsing. Just call a function. The server-client boundary becomes invisible.
4. API Routes: The Traditional REST Layer
API Routes are your traditional HTTP endpoints. They give you full control over the request and response.
What Do They Do
They expose HTTP endpoints that external services, mobile apps, or third-party integrations can call. Unlike Server Actions (which are for your own app), API Routes are for when you need a public-facing REST API with full control over headers, status codes, and response formats.
Here's what an API Route looks like:
1 // app/api/users/route.ts 2 import { NextRequest, NextResponse } from "next/server"; 3 // Import from server-only module - API Routes can use them too! 4 import { getUsers } from "@/lib/server-only/database"; 5 6 export async function GET(request: NextRequest) { 7 // Call server-only module directly 8 const users = await getUsers(); 9 return NextResponse.json(users); 10 } 11 12 export async function POST(request: NextRequest) { 13 const body = await request.json(); 14 // Handle the request 15 return NextResponse.json({ success: true }); 16 }
When to Use Them
- •Building a public API for external consumers
- •Webhooks from third-party services
- •Fine-grained control over headers, status codes, or response format
- •Integrating with non-Next.js clients (mobile apps, external services)
When NOT to Use Them
- •Internal form handling (use Server Actions instead)
- •Fetching data for your own pages (use Server Components)
Honestly? In modern Next.js apps, you'll use API Routes less than you think. Server Actions handle a big chunk of what API Routes used to do for data mutation. The other big chunk was taken over by server actions for data fetching and rendering.
The Mental Model: When to Use What
Let me give you a decision tree:
Need to display data on initial page load?
Server Component
Need to handle a form submission or mutation?
Server Action
Need to expose an endpoint for external services or webhooks?
API Route
Need to keep secrets, access databases, or run heavy server business logic? Or if you want to create a peice of code that can be used by any of the above 3?
Server-Only Module
Common Pitfalls and How to Avoid Them
Pitfall #1: Mixing Client and Server Code
Problem:
1 "use client"; 2 import { db } from "@/lib/database"; // This will error or bundle server code
Solution:Use Server Actions to bridge the gap:
1 "use client"; 2 import { getData } from "@/actions/data"; // Server Action handles it
Pitfall #2: Not Using "server-only"
Problem: Forgetting to mark sensitive modules means they could accidentally bundle to the client.
import "server-only"
at the top of modules with secrets:1 import "server-only"; 2 // Now this file will error if imported on the client
Pitfall #3: Creating API Routes When Server Actions Would Work
Problem: Building a REST API just for your own form:
1 // Unnecessary API route 2 export async function POST(request: NextRequest) { 3 const data = await request.json(); 4 // Do stuff 5 } 6 7 // Then fetching it from the client 8 fetch("/api/my-action", { method: "POST", body: JSON.stringify(data) });
Solution: Use a Server Action:
1 "use server"; 2 export async function myAction(data: FormData) { 3 // Do stuff directly 4 }
Conclusion
Next.js backend isn't complicated—it's just layered. Once you understand that Server-Only Modules are your foundation and the other three are just different ways to expose that foundation, it all clicks.
- Server-Only Modules: Your business logic and secrets
- Server Components: Generate HTML from data
- Server Actions: Handle mutations and forms
- API Routes: Expose REST endpoints
Now go build something great. And remember: when in doubt, it probably belongs in a server-only module.