Next.js: The Full-Stack Framework
Where React meets the server
You've now learned React, APIs, and Node.js as separate things. Three different tools, three different mental models, three different sets of wiring to connect them together. What if there was a single framework that snapped them all together like a Swiss Army knife -- one handle, every blade you need? That's Next.js. It takes React for your UI, Node.js for your server, and a routing system that requires zero configuration, and fuses them into one cohesive toolkit. And here's the part that should make this feel real: this learning app you're reading right now? It's built with Next.js. Every page, every chapter, every quiz. You're not just learning about the tool -- you're already using it.
Why Frameworks Exist: The Pain Comes First
Before we talk about what Next.js gives you, let's feel what life is like without it. Because you won't appreciate the solution until you've tasted the problem.
Here's what building a production React app from scratch looks like. Step one: choose a bundler (Webpack? Vite? Parcel?). Step two: configure routing -- install React Router, set up your route definitions, handle nested layouts manually. Step three: figure out server-side rendering for SEO -- because a blank HTML page with a JavaScript bundle is invisible to search engines. Step four: set up code-splitting so you don't ship a five-megabyte JavaScript file to every visitor. Step five: handle environment variables. Step six: optimize images. Step seven: build your API layer. Step eight: make sure it all plays nicely together.
Each step means installing packages, writing configuration files, and gluing things together with code that has nothing to do with your actual application. You're building scaffolding instead of building rooms.
Next.js (created by Vercel) was built to make all that pain disappear. It's a "batteries-included" framework that makes smart, opinionated choices about bundling, routing, rendering, and optimization so you can focus on the thing that matters: your application's features. The React team themselves now recommend starting with a framework like Next.js rather than plain React.
Before reading on, think about this: why would the React team -- the people who built React -- tell you NOT to use React by itself?
Because React is a UI library, not a complete solution. It renders components beautifully, but it doesn't have opinions about routing, data fetching, server rendering, or deployment. A framework like Next.js fills those gaps. It's not replacing React -- it's giving React a home with plumbing, electricity, and a roof. And crucially, Next.js isn't just a frontend framework. It runs code on both the client and the server, managing the boundary between the two. This means you can build pages, components, API endpoints, and data fetching logic in a single project, without needing the separate Express server you built in Chapter 5.
Rendering Strategies: When Do You Bake the Cake?
This is where most beginners' eyes glaze over. SSR, CSR, SSG, RSC -- it sounds like an eye chart at the optometrist. But every one of these acronyms answers the same simple question: when and where does the HTML for your page get created?
Think of it like a bakery. There are four ways to get a cake to a customer.
Made to order (SSR -- Server-Side Rendering): a customer walks in, you bake their cake from scratch right then. They wait a moment, but they get a fresh, personalized cake. In web terms, every time someone visits the page, the server runs your React components, generates the HTML, and sends it. Fresh content every time, great for SEO, but the server does work on every single request.
Pre-baked (SSG -- Static Site Generation): you bake fifty cakes before the shop opens. Customers walk in and grab one immediately -- instant, no waiting. In web terms, the HTML is generated once at build time and served as static files. Blazing fast and perfect for content that doesn't change often, like a blog post or a marketing page. The downside? If you change the recipe, you have to re-bake everything.
Assemble-at-home (CSR -- Client-Side Rendering): you ship the customer a box of ingredients with instructions. They assemble the cake themselves in their own kitchen (the browser). This is how plain React works -- the server sends a minimal HTML page with a big JavaScript bundle, and the browser does all the rendering. The problem? The customer stares at an empty box until the JavaScript downloads and runs. And search engines looking through the window just see... an empty box.
Chef's special ingredients (RSC -- React Server Components): this is the newest approach, and it's what makes Next.js's App Router special. Some parts of the cake are prepared in the kitchen by the chef (server), and only the parts that need customer interaction -- like the "write your name on it" step -- are sent as a kit. In code terms, Server Components run only on the server and send finished HTML, never shipping their JavaScript to the browser. Client Components handle the interactive bits. The result: smaller bundles, faster pages, and the ability to query databases directly from your components.
Look at the code examples below. The `PostList` component queries a database directly -- no API endpoint needed. That code never reaches the browser. The `LikeButton`, in a separate file marked with "use client" at the very top, ships JavaScript because it needs to respond to clicks. Two files, two different strategies, working together seamlessly.
// app/posts/page.tsx
// Server Component (default in Next.js App Router)
// This code ONLY runs on the server -- zero JS sent to browser
import { LikeButton } from "./like-button"
async function PostList() {
// You can directly fetch data or query a database here!
const posts = await db.query("SELECT * FROM posts LIMIT 10")
return (
<div className="space-y-4">
{posts.map((post) => (
<article key={post.id}>
<h2 className="text-xl font-bold">{post.title}</h2>
<p>{post.excerpt}</p>
<LikeButton postId={post.id} />
</article>
))}
</div>
)
}// app/posts/like-button.tsx
// "use client" MUST be the very first line of the file
"use client"
import { useState } from "react"
export function LikeButton({ postId }: { postId: number }) {
const [liked, setLiked] = useState(false)
return (
<button onClick={() => setLiked(!liked)}>
{liked ? "ā¤ļø Liked" : "š¤ Like"}
</button>
)
}File-Based Routing: Your Folders Are Your Website
This is one of those ideas that, once you see it, you can't believe it wasn't always done this way.
In most React apps, you have to manually configure routing. You install React Router, define a list of paths, map each path to a component, handle nested layouts yourself, and pray nothing breaks when you reorganize. In Next.js, you skip all of that. Your folder structure IS your website. Create a file, get a page. That's it.
In the App Router (Next.js 13+), routes live inside the `app` directory. Each folder represents a URL segment. Drop a `page.tsx` file inside that folder, and that folder becomes a route. So `app/page.tsx` is your homepage (`/`). `app/about/page.tsx` is your about page (`/about`). `app/blog/[slug]/page.tsx` -- notice the square brackets -- is a dynamic route that matches any URL like `/blog/my-first-post`, capturing the "slug" as a parameter you can use in your code.
Here's a question: if you wanted to add a `/contact` page to your Next.js app, what would you do?
Create a folder called `contact` inside `app`, and put a `page.tsx` file in it. Done. No router configuration, no imports, no wiring. The file system did the work.
But it gets better. Next.js uses specially named files to handle patterns that every web app needs. `layout.tsx` wraps all pages in its folder (and subfolders) with shared UI like navigation bars -- no prop-drilling required. `loading.tsx` automatically shows a loading skeleton while page data is being fetched. `error.tsx` catches errors and displays a fallback instead of crashing your entire app. These conventions mean you spend less time wiring plumbing and more time building the rooms people actually see.
Look at the directory structure in the first code example. That's not a configuration file. That's just... folders and files. And yet it defines an entire website with a home page, an about page, a blog with dynamic posts, a dashboard with nested layouts, and an API endpoint. Convention over configuration at its finest.
app/
āāā layout.tsx # Root layout (wraps ALL pages)
āāā page.tsx # Home page ā /
āāā about/
ā āāā page.tsx # About page ā /about
āāā blog/
ā āāā page.tsx # Blog index ā /blog
ā āāā [slug]/
ā āāā page.tsx # Blog post ā /blog/my-post
ā āāā loading.tsx # Loading state for this page
āāā dashboard/
ā āāā layout.tsx # Dashboard layout (nested)
ā āāā page.tsx # Dashboard home ā /dashboard
ā āāā settings/
ā āāā page.tsx # Settings ā /dashboard/settings
āāā api/
āāā users/
āāā route.ts # API endpoint ā /api/users// app/blog/[slug]/page.tsx
// Dynamic route -- [slug] captures the URL segment
interface BlogPostPageProps {
params: Promise<{ slug: string }>
}
export default async function BlogPostPage({
params,
}: BlogPostPageProps) {
const { slug } = await params
// Fetch the post using the slug from the URL
const post = await getPostBySlug(slug)
if (!post) {
notFound() // Shows the 404 page
}
return (
<article className="prose mx-auto max-w-2xl">
<h1>{post.title}</h1>
<time>{post.publishedAt}</time>
<div>{post.content}</div>
</article>
)
}API Routes: Your Express Server, Built In
Remember the Express server you built in Chapter 5? The one with routes for GET, POST, and DELETE? Next.js has that same capability baked right in. No separate server. No extra port. No CORS headaches.
In the App Router, you create a `route.ts` file inside the `app/api/` directory, and it becomes a live HTTP endpoint. Each file exports functions named after HTTP methods -- `GET`, `POST`, `PUT`, `DELETE` -- and those functions receive a standard Web API `Request` object and return a `Response`. This is real backend code. It runs on the server, has access to environment variables, can connect to databases, and its logic never reaches the browser.
Compare the code example below to the Express server from the previous chapter. Same idea -- a todo API with GET and POST handlers -- but notice how much less ceremony there is. No `app.use()` middleware setup, no `app.listen()` call, no CORS configuration. Next.js handles the infrastructure; you write the logic.
Before reading on, think about this: if your Next.js app serves both the web pages AND the API, what does that mean for deployment?
It means you deploy one thing. One project, one deployment, one URL. Your pages and your API live under the same roof. For the learning app you're using right now, there's no separate Express server running somewhere -- the pages you read and the data they fetch all come from a single Next.js application.
Of course, for complex systems with heavy backend requirements -- microservices, real-time data pipelines, CPU-intensive processing -- you might still want a dedicated backend. But for a huge number of projects, Next.js API routes give you everything you need. In Chapter 7, you'll see how to connect these API routes to a real database, replacing the in-memory arrays we've been using with persistent storage.
// app/api/todos/route.ts
import { NextResponse } from "next/server"
// In-memory store (use a real database in production!)
const todos = [
{ id: 1, text: "Learn Next.js", completed: false },
]
// GET /api/todos
export async function GET() {
return NextResponse.json(todos)
}
// POST /api/todos
export async function POST(request: Request) {
const body = await request.json()
const { text } = body
if (!text) {
return NextResponse.json(
{ error: "Text is required" },
{ status: 400 }
)
}
const newTodo = {
id: Date.now(),
text,
completed: false,
}
todos.push(newTodo)
return NextResponse.json(newTodo, { status: 201 })
}
// app/api/todos/[id]/route.ts
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const index = todos.findIndex((t) => t.id === Number(id))
if (index === -1) {
return NextResponse.json(
{ error: "Todo not found" },
{ status: 404 }
)
}
todos.splice(index, 1)
return new Response(null, { status: 204 })
}- Next.js is the Swiss Army knife of web development -- React, Node.js, routing, rendering, and API endpoints in one framework. It eliminates the dozens of configuration decisions you'd face building a React app from scratch.
- Rendering strategies answer one question: when is the cake baked? SSR (made to order), SSG (pre-baked), CSR (assemble-at-home), and RSC (chef's special ingredients) each fit different situations. Server Components run only on the server, slashing the JavaScript sent to browsers.
- File-based routing turns your folder structure into your website's URL map. Create a folder, add a page.tsx, and you have a new page. Special files like layout.tsx, loading.tsx, and error.tsx handle common patterns automatically.
- API routes let you build backend endpoints right inside your Next.js project -- the same idea as Express from Chapter 5, but with less ceremony and no separate server to manage.
- Server Components can directly access databases and APIs without exposing code to the browser, while Client Components (marked with 'use client') handle interactivity -- two tools on the same knife, each for the right job.
What is the main advantage of React Server Components?