Inksh logoContactAbout usPrivacy
Author name Hemant Bhatt

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

Next.js Backend Handbook - Database, Server, and Routes illustrated as a coffee brewing system

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:

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.

lib/server-only/database.ts
1import "server-only";
2
3export 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:

lib/server-only/database.ts
1import "server-only";
2
3export 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:

app/profile/[id]/page.tsx
1import { getUserProfile } from "@/lib/server-only/database";
2
3export 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:

actions/profile.ts
1"use server";
2
3import { revalidatePath } from "next/cache";
4import { updateUserProfile } from "@/lib/server-only/database";
5
6export 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.

components/ProfileForm.tsx
1"use client";
2
3import { saveProfile } from "@/actions/profile";
4
5export 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:

app/api/users/route.ts
1// app/api/users/route.ts
2import { NextRequest, NextResponse } from "next/server";
3// Import from server-only module - API Routes can use them too!
4import { getUsers } from "@/lib/server-only/database";
5
6export async function GET(request: NextRequest) {
7 // Call server-only module directly
8 const users = await getUsers();
9 return NextResponse.json(users);
10}
11
12export 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:

components/BadExample.tsx
1"use client";
2import { db } from "@/lib/database"; // This will error or bundle server code

Solution:Use Server Actions to bridge the gap:

components/GoodExample.tsx
1"use client";
2import { 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.

Solution: Always add import "server-only" at the top of modules with secrets:
lib/server-only/secrets.ts
1import "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:

Unnecessary complexity
1// Unnecessary API route
2export async function POST(request: NextRequest) {
3 const data = await request.json();
4 // Do stuff
5}
6
7// Then fetching it from the client
8fetch("/api/my-action", { method: "POST", body: JSON.stringify(data) });

Solution: Use a Server Action:

Much simpler
1"use server";
2export 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.