Modern Next.js: Routing, Layouts, Server Components, API Routes & Server Actions
July 20, 202610 min read
JavaScriptReactWeb DevelopmentNext.js
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:
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.
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 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.
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 } }); }
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.
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.