Full-Stack with Next.js
Server Components, route handlers, and one mental model for frontend + backend.
What Next.js is
Next.js is a full-stack React framework built by Vercel. Plain React only handles the frontend; Next adds routing, server-side rendering, an HTTP server, and dozens of production niceties (image optimization, font loading, caching). It is the default way to build production React apps today.
The key insight: in Next.js, the same code can run on the server (at build time or per request) AND in the browser. You stop thinking of frontend and backend as separate projects.
The App Router
Modern Next uses file-based routing under the `app/` directory. Each folder is a URL segment; `page.tsx` is the page; `layout.tsx` wraps it; `route.ts` defines an API endpoint.
app/
├── layout.tsx # wraps all pages
├── page.tsx # /
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
└── api/
└── hello/
└── route.ts # GET /api/helloServer Components
By default in the App Router, every component is a Server Component. It runs ONLY on the server, can be `async`, can talk directly to your database, and never ships JS to the browser. This is a huge deal — most pages don't need any client-side JS at all.
// app/repos/page.tsx — Server Component (default)
export default async function Page() {
const data = await fetch("https://api.github.com/repos/vercel/next.js")
.then(r => r.json());
return (
<main>
<h1>{data.full_name}</h1>
<p>{data.stargazers_count.toLocaleString()} stars</p>
</main>
);
}Read that again: an `async` React component that calls `fetch` directly, with no `useEffect` and no loading state. It runs on the server, the HTML is sent to the browser, and that's it.
Client Components
When you need interactivity — state, effects, event handlers — add `"use client";` to the top of the file. That marks the component (and its children) as a Client Component. They run in the browser, like normal React.
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Clicked {n} times</button>;
}Route handlers (API endpoints)
// app/api/hello/route.ts
export async function GET() {
return Response.json({ message: "Hello from the server" });
}
export async function POST(req: Request) {
const body = await req.json();
return Response.json({ ok: true, received: body });
}Data flow patterns
- Read static data → Server Component, fetch directly. Cache automatically.
- Write data → Server Action (`async function` marked with `"use server"`), invoked from a form or button.
- Live, interactive data → Client Component + useEffect + fetch.
- Authentication state → Server Component reads cookies; pass user info as props down.