JavaScript28 min readAdvanced

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.

text
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/hello

Server 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.
💡 Tip
When in doubt, default to Server Components. Add `"use client"` only when you genuinely need state or browser APIs. Less JS in the bundle = faster page loads.