Skip to main content
Madhukar
All Articles

Modern Next.js: Routing, Layouts, Server Components, API Routes & Server Actions

July 20, 202610 min read
JavaScriptReactWeb DevelopmentNext.js
Modern Next.js: Routing, Layouts, Server Components, API Routes & Server Actions

Can a React application have both frontend and backend inside the same project?

For most of React’s history, the answer was practically “no, not really.” A React app was the frontend — a separate Express server, or a separate API entirely, handled the backend. Two codebases, two deployments, one connection between them held together by fetch calls.

Modern Next.js changes that answer. Routing, layouts, Server Components, API Routes, and Server Actions together let a single project hold your UI and your backend logic — genuinely, not just as a convenient folder structure. This guide walks through that entire modern architecture, piece by piece, using the running example of a SaaS dashboard application throughout.

1. Evolution of Next.js

How Next.js has evolved over time

Early Next.js focused mainly on solving rendering — giving React apps server-rendered and statically generated pages via the pages/ directory. Over several years, it steadily absorbed more of the "everything around the UI" problem: routing conventions, image optimization, API endpoints, and eventually a fundamentally new architecture built around React Server Components.

Why the App Router was introduced

The pages/ directory handled routing well, but it wasn't designed around nested layouts or Server Components from the ground up — both had to be approximated with workarounds. The App Router (the app/ directory) was built specifically to make these first-class, native capabilities.

Problems it solves in large applications

  • Deeply nested shared UI (a dashboard within a dashboard) without prop-drilling layout state
  • Fine-grained control over what runs on the server versus the browser, route by route
  • Colocating a route’s UI, loading state, error handling, and data fetching in one place

Modern full-stack React development

With the App Router, Server Components, and Server Actions combined, a Next.js project can genuinely function as both frontend and backend — the dividing line this guide keeps coming back to.

2. Understanding the App Router

Folder-based routing architecture

Inside the app/ directory, folders define URL segments, and special files within each folder (page.js, layout.js, loading.js, error.js) define that segment's behavior.

Route creation

app/
page.js → /
dashboard/
page.js → /dashboard

Adding a route is as simple as adding a folder with a page.js file — no separate route configuration to maintain.

Dynamic routes

Square brackets create dynamic segments that capture part of the URL as a variable:

app/
projects/
[projectId]/
page.js → /projects/:projectId

Nested routes

Folder nesting mirrors URL nesting directly:

app/
dashboard/
settings/
page.js → /dashboard/settings

Route groups

Wrapping a folder name in parentheses — like (marketing) — groups routes for organizational purposes without adding that name to the URL. This is how large apps separate concerns like (marketing) pages from (dashboard) pages while keeping both under a clean URL structure.

Organizing large applications

app/
(marketing)/
page.js → /
pricing/
page.js → /pricing
(dashboard)/
dashboard/
page.js → /dashboard
settings/
page.js → /dashboard/settings

3. Layouts in Next.js

Why layouts exist

A SaaS dashboard’s sidebar and top navigation shouldn’t re-render, re-fetch, or flicker every time a user clicks between /dashboard, /dashboard/settings, and /dashboard/billing. Layouts exist to hold that shared UI stable across navigations within a section.

Shared UI across routes

A layout.js file wraps every route nested inside its folder, rendering once and persisting as child pages change underneath it.

Nested layouts

Layouts compose naturally: a root layout might hold a global header; a dashboard/layout.js adds a sidebar specific to dashboard routes, without duplicating the root layout's markup.

Persistent layouts

Because a layout isn’t re-rendered on every navigation within its scope, interactive elements inside it — an open dropdown, a scroll position in a sidebar — persist naturally as the user moves between child pages.

Building scalable applications using layouts

Nesting layouts by feature keeps each layer of shared UI owned by the section of the app it actually belongs to — a pattern that scales cleanly as more sections get added.

4. Route Organization Strategies

Feature-based organization

Grouping routes and their related components by feature (dashboard/, billing/, auth/) rather than by file type keeps everything related to one part of the product close together — easier to navigate as the app grows.

Dashboard applications

A typical SaaS dashboard groups its authenticated routes under a shared layout with a consistent sidebar, while public marketing routes live under a separate route group with their own layout entirely.

Authentication flows

Login, signup, and password-reset pages are often grouped together — commonly under a route group like (auth) — sharing a minimal, centered layout distinct from the rest of the app.

Public vs protected routes

Route groups combined with layout-level or middleware-level checks (covered in Section 11) let an application clearly separate what’s publicly reachable from what requires authentication.

Scaling route structures

As an application grows past a handful of routes, the folder tree itself becomes a form of documentation — a new developer can often understand the app’s shape just by browsing app/, without reading a separate routing configuration file.

5. Server Components

Why Server Components were introduced

Traditionally, every React component’s code shipped to the browser, whether or not it needed to run there. Server Components let components that only fetch and display data run entirely on the server, never becoming part of the browser’s JavaScript bundle at all.

How they differ from Client Components

In the App Router, components are Server Components by default. Only components explicitly marked with "use client" become Client Components that run in the browser.

Benefits of server-side execution

Server Components can query a database or call internal services directly, without exposing that logic or those credentials to the browser — and their code simply never becomes part of what the client has to download.

Reduced JavaScript bundles

Every component that stays a Server Component is JavaScript the browser never has to download, parse, or execute — a direct, compounding win as an application grows.

Performance improvements

Combined with server-side data fetching (Section 12), Server Components let a page arrive with real content already rendered, rather than a shell waiting on client-side fetches to complete.

// Server Component (default) — a dashboard summary card
async function RevenueCard() {
const revenue = await db.orders.sumRevenue(); // direct DB access, safely server-side
return <div className="card">Total Revenue: ${revenue}</div>;
}

6. Client Components

Interactive UI requirements

Anything requiring genuine browser-side interactivity — clicking, typing, dragging — needs to run in the browser, and therefore needs to be a Client Component.

State management

useState, useReducer, and any component holding local, changing state must be a Client Component — state and re-rendering, as covered in earlier parts of this series, are fundamentally client-side concepts.

Browser APIs

Anything touching window, localStorage, geolocation, or other browser-only APIs requires a Client Component, since these simply don't exist in a server environment.

Event handling

onClick, onChange, onSubmit handlers all require a Client Component — Server Components render once on the server and can't respond to browser events directly.

"use client";

function NotificationToggle() {
const [enabled, setEnabled] = useState(true);
return (
<button onClick={() => setEnabled(!enabled)}>
{enabled ? "Notifications On" : "Notifications Off"}
</button>
);
}

When Client Components are necessary

Use them specifically where interactivity, state, or browser APIs are genuinely required — not as a default wrapper applied out of habit to every component.

7. Mixing Server and Client Components

Modern composition patterns

The recommended pattern is to keep Server Components at the top of the tree wherever possible, and push Client Components as far down (and as narrow) as the actual interactivity requires — a LikeButton should be a Client Component; the BlogPost wrapping it usually doesn't need to be.

Data fetching strategies

Server Components fetch data directly during their own render on the server; Client Components that need data typically receive it as props from a Server Component parent, rather than fetching it themselves after mounting.

Performance considerations

Every additional Client Component (and everything it imports) adds to the browser’s JavaScript payload. Keeping the interactive surface area deliberately small keeps the overall bundle smaller as the application grows.

Building maintainable applications

A useful mental default: start every component as a Server Component, and only add "use client" when you hit a specific requirement — state, events, or browser APIs — that actually needs it. This keeps the "why is this a Client Component" question always answerable.

8. API Routes

What API Routes are

API Routes (app/api/.../route.js) let a Next.js project expose traditional HTTP endpoints — GET, POST, PUT, DELETE — directly inside the same project as the frontend.

Why they exist

Not every backend interaction fits neatly into a Server Component’s “fetch data during render” model — webhooks from external services, endpoints called from non-Next.js clients, or APIs meant to be consumed outside the app all still need a conventional HTTP endpoint.

Backend functionality inside Next.js

// app/api/orders/route.js
export async function GET() {
const orders = await db.orders.findMany();
return Response.json(orders);
}

export async function POST(request) {
const body = await request.json();
const order = await db.orders.create({ data: body });
return Response.json(order, { status: 201 });
}

Authentication endpoints

Login, logout, and token-refresh endpoints are common API Route use cases — especially when integrating with external auth providers via callback URLs.

CRUD operations

API Routes are a natural fit for exposing standard create/read/update/delete operations on a resource, especially when that resource also needs to be reachable from outside the Next.js app itself.

Internal APIs

Even within a single Next.js app, API Routes are useful wherever a request needs to be handled outside the render cycle — file uploads, background job triggers, or scheduled webhook receivers.

9. Server Actions

Why Server Actions were introduced

Server Actions were introduced to remove a specific, repetitive pattern: building a dedicated API Route just to handle one form submission from within your own app. They let a Client Component call a server-side function directly, without manually wiring up a fetch call to a separate endpoint.

Reducing API boilerplate

// app/dashboard/actions.js
"use server";

export async function updateProfile(formData) {
const name = formData.get("name");
await db.users.update({ where: { id: currentUserId() }, data: { name } });
}

Form handling

import { updateProfile } from "./actions";

function ProfileForm() {
return (
<form action={updateProfile}>
<input name="name" />
<button type="submit">Save</button>
</form>
);
}

No manual fetch, no manually parsing a request body, no separate route file — the form's action calls the server function directly.

Mutations

Server Actions are purpose-built for mutations — creating, updating, or deleting data in response to a user action — rather than for general-purpose data fetching.

Data updates

Because Server Actions run on the server, they can safely access the database directly, then trigger the UI to reflect the change (often paired with cache revalidation, covered in Section 12).

Modern Next.js workflows

Server Actions represent a genuine shift: instead of “frontend calls an API which calls the database,” the flow can simply become “frontend calls a server function which talks to the database” — one fewer layer to build and maintain for many common interactions.

10. API Routes vs Server Actions

Architectural differences

API Routes expose a conventional HTTP endpoint, reachable by anything that can make an HTTP request. Server Actions are functions callable directly from your own Next.js Client Components — not a general-purpose public endpoint by nature.

Use cases

Tradeoffs

API Routes require more boilerplate (defining the route, the request/response handling, and the client-side fetch call) but offer a stable, conventional HTTP contract. Server Actions are far less boilerplate for in-app interactions but are tightly coupled to the Next.js app calling them — they’re not designed to be a public API surface.

Performance considerations

Both run server-side and avoid shipping backend logic to the client. Server Actions can integrate more directly with Next.js’s caching and revalidation model for the specific route that called them.

Developer experience comparison

Server Actions dramatically reduce the ceremony around simple, in-app mutations — no separate file, no manual fetch, no manually shaping a JSON request. API Routes remain the right choice the moment something outside your own frontend needs to call that logic.

11. Authentication Architecture

Login systems

A typical modern flow: a login form (Client Component) submits credentials via a Server Action or API Route, the server verifies them, and a session token or cookie is issued to identify the user on subsequent requests.

Protected routes

Routes that require authentication check for a valid session before rendering — typically at the layout level (for a whole authenticated section) or via middleware (for broader, cross-cutting protection).

Session management

Sessions are commonly stored as a signed, HTTP-only cookie — readable by the server on each request, but inaccessible to client-side JavaScript, which helps protect against common token-theft attacks.

Middleware concepts

Next.js middleware runs before a request reaches its route, making it a natural place to check authentication status and redirect unauthenticated users away from protected sections — centrally, rather than repeating the check in every individual route.

// middleware.js
export function middleware(request) {
const session = request.cookies.get("session");
if (!session && request.nextUrl.pathname.startsWith("/dashboard")) {
return Response.redirect(new URL("/login", request.url));
}
}

Authentication flows

12. Data Fetching Patterns

Server-side data fetching

Server Components fetch data directly during render, on the server — the resulting HTML already contains real data by the time it reaches the browser.

Client-side data fetching

Client Components sometimes still fetch their own data — typically for data that updates frequently after the initial load, or that depends on client-only state (like a live search-as-you-type result list).

Streaming

Next.js can stream a page’s HTML to the browser in pieces, sending fast-to-render parts immediately while slower data-dependent sections load in — paired with a loading.js fallback for the slower piece, so users see meaningful content immediately instead of waiting for the entire page.

Caching

Next.js caches rendered output and fetch results at multiple levels by default, avoiding redundant work for content that hasn’t changed — a significant part of how it achieves strong baseline performance without manual tuning.

Revalidation concepts

Revalidation tells Next.js a cached result is stale and needs refreshing — either on a time interval (similar to ISR from the previous article in this series) or on-demand, immediately after a Server Action performs a mutation, so the UI reflects the change right away.

13. Building Large-Scale Applications

Folder structures

Large Next.js applications typically combine route-based organization (app/) with a separate, feature-oriented structure for shared logic — components, hooks, and utilities grouped by the feature they serve rather than by file type alone.

Feature separation

Keeping a feature’s routes, components, Server Actions, and types colocated (or at least clearly grouped) reduces the cognitive overhead of working within just one part of a large codebase.

Team scalability

Clear route groups and feature boundaries let different teams or developers own different sections of the application with minimal overlap or merge conflicts — the folder structure itself enforces some of that separation.

Maintainability

Because routing, layouts, and data fetching all follow the same file-based conventions throughout, a developer moving between features doesn’t have to learn a new pattern each time — the architecture is genuinely consistent across the codebase.

Production architecture

14. Performance Optimization

Server rendering

Rendering on the server by default means the browser receives meaningful HTML immediately, rather than an empty shell waiting on client-side JavaScript.

Streaming

Streaming lets fast content reach the user immediately while slower, data-heavy sections load progressively — improving perceived performance even when total load time is unchanged.

Reduced client bundles

Every component that stays a Server Component is JavaScript the browser never downloads — a compounding advantage as an application’s feature set grows.

Selective hydration

Next.js can hydrate (make interactive) different parts of a streamed page as their JavaScript becomes ready, rather than waiting for the entire page’s JavaScript to load before any of it becomes interactive.

Modern optimization strategies

Combined, these mean a well-structured modern Next.js app can deliver fast initial content, a small JavaScript footprint, and quick interactivity — without hand-tuning most of it manually, simply by following the architecture’s natural defaults.

15. The Future of Full-Stack React

Why Next.js is becoming the default

The combination covered in this guide — routing, layouts, Server Components, API Routes, and Server Actions — collapses what used to be two separate applications (a React frontend, a backend API) into one coherent, consistently-structured project.

Backend + Frontend in one framework

A modern Next.js app can genuinely own its entire stack: UI, server-side data access, mutations via Server Actions, and external-facing endpoints via API Routes — all governed by the same file-based conventions.

Modern web development trends

The broader industry direction — server-aware rendering, reduced client JavaScript by default, colocated frontend and backend logic — is increasingly the starting assumption for new React projects, not an advanced, optional pattern.

Enterprise adoption

For teams building and maintaining large, long-lived applications, a framework that enforces consistent structure across routing, data access, and mutations reduces the amount of custom architecture every team has to independently design, document, and onboard new developers onto.

Final Takeaway

Every piece covered here answers the same underlying question from a different angle: where should this logic live, and who should run it — the server, or the browser? The App Router organizes what renders where. Server and Client Components decide where each piece of UI actually executes. API Routes and Server Actions decide how the frontend triggers backend work, for external and internal callers respectively. Put together, they’re not just a performance optimization — they’re a genuinely different, more unified way to build a full application in a single, consistently structured codebase.

Frequently Asked Questions

Do I have to choose between API Routes and Server Actions for my whole app?

> No — most production Next.js apps use both. Server Actions handle in-app form submissions and mutations; API Routes handle webhooks, external integrations, and any endpoint that needs to be called from outside the app itself.

Are Server Components a replacement for a traditional backend?

> Partially. They let components fetch data server-side directly, but a full backend often still needs dedicated API Routes for external integrations, background jobs, and anything not tied to rendering a specific page.

Is middleware the right place for all authentication logic?

> Middleware is well-suited for broad, cross-cutting checks like “is there a valid session at all.” More granular, resource-specific authorization (like “can this specific user edit this specific project”) is usually still better handled inside the relevant Server Component, Server Action, or API Route itself.

Do Server Actions work without JavaScript enabled in the browser?

> Forms using Server Actions can work as a progressive enhancement of a standard HTML form submission, though the exact behavior depends on how the form and action are set up. This is one of several nuanced implementation details worth checking against current Next.js documentation for your specific version.

Originally published by Mr Madhukar

Read the complete article on Medium with full formatting & reader responses.