Type something to search...
Next.js Getting Started

Next.js Getting Started

If you open the official Next.js documentation for the first time, the "Getting Started" section looks deceptively small. It's really just a table of contents — one line per topic, no code, no deep explanations. But that brevity is the point. Before you touch a single file, it helps to understand what that table of contents is actually describing: a complete mental model for how a modern, full-stack Next.js application is put together, from the moment a request hits your server to the moment pixels land on a user's screen.

This article is that mental model. It won't teach you every API in depth — each of the eighteen topics that follow in this section gets its own dedicated article — but it will give you the map. By the end, you should understand how routing, rendering, data fetching, caching, and deployment all fit together in the App Router, so that when you read the deep dive on, say, Caching or Server and Client Components, you already know where that piece sits in the bigger picture.

What "App Router" Actually Means

Next.js ships with two different routing systems: the older Pages Router (everything lives under a pages/ directory, one file per route) and the App Router (everything lives under an app/ directory, built around React Server Components). Since Next.js 13, the App Router has been the recommended default for new projects, and as of Next.js 15 and 16 it's where nearly all new features land first — things like Server Actions, use cache, and Cache Components only exist in the App Router.

The name is a little misleading if you're coming from other frameworks. "App Router" doesn't mean "the router for single-page apps." It means a router built around nested layouts, colocated data fetching, and a rendering pipeline that treats "static" and "dynamic" as a spectrum rather than a binary choice you make once at the top of a page. That last part is the biggest conceptual shift for anyone coming from the Pages Router or from a plain client-rendered React app: instead of picking getStaticProps or getServerSideProps for an entire page, you decide per-component, per-fetch, whether something should be cached, revalidated, or always fresh.

Prerequisite Knowledge

The official docs are upfront about one thing: they assume you already know HTML, CSS, JavaScript, and React. This isn't a React tutorial, and if terms like "component," "props," "hooks," or "the virtual DOM" don't mean anything to you yet, it's worth spending time with React's own documentation first. Next.js doesn't hide React from you — it extends it. A Next.js component is a React component, useState and useEffect work exactly as they do in any React app, and if you already understand useEffect's dependency array, you understand it here too.

Where Next.js adds new concepts, it does so as an extension of things React already has: React Server Components (a React feature, not a Next.js-specific one) become the default rendering mode for every component under app/. The fetch function gets extended with caching options. New file conventions (layout.tsx, page.tsx, loading.tsx) give you hooks into the framework's routing and rendering behavior without requiring you to learn a new templating language or component model.

If you've never used React before, don't skip ahead — every later article in this series assumes you're comfortable writing a functional component and using at least useState.

Setting Up: What create-next-app Gives You

Every App Router project starts with create-next-app, which scaffolds the directory structure this whole section is about:

npx create-next-app@latest my-app

Running that command with the defaults gets you an app/ directory containing at minimum layout.tsx and page.tsx, a public/ directory for static assets, a next.config.js (or .ts, or .mjs) for framework configuration, and (if you accept TypeScript, which you should) a tsconfig.json. The next article in this series, Installation, walks through every prompt the CLI asks and what each answer does — this article is just establishing why that scaffold looks the way it does.

The two files worth staring at immediately are these:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}
// app/page.tsx
export default function HomePage() {
  return <h1>Hello, Next.js</h1>;
}

That's a complete, runnable Next.js application. No routing configuration, no <Router> component, no manifest describing your routes. The file at app/page.tsx becomes the / route because of where it sits in the file system. The file at app/layout.tsx wraps every page in your app in shared markup — in this case, the <html> and <body> tags that every page needs exactly once. This is the core idea you'll see repeated throughout the rest of this section: the file system is the API. Where you put a file, and what you name it, determines what it does.

The Rendering Model: Server by Default

The single most important idea to internalize before you write real App Router code is this: every component in app/ is a Server Component unless you explicitly opt out. That's a reversal from the client-first world most React developers came from, where create-react-app and Vite templates render everything in the browser by default.

A Server Component runs only on the server (or at build time, if the route is static). It never ships its own JavaScript to the browser. It can be async, it can query a database directly, it can read a file from disk, and none of that code — not even the import statements for your database driver — ends up in the client bundle. This is why the Fetching Data article can show you await fetch(...) or await db.query(...) directly inside a component body with no useEffect, no loading state, and no client-side request waterfall.

The moment you need interactivity — a click handler, useState, useEffect, browser-only APIs like window or localStorage — you opt into client rendering with the 'use client' directive at the top of the file:

"use client";

import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}

The Server and Client Components article goes deep on the rules here (Server Components can render Client Components, but not the reverse; props passed from server to client must be serializable), but the mental model to carry forward is: default to server, opt into client only where you need interactivity. A common mistake for developers new to the App Router is slapping 'use client' at the top of every file out of habit, which quietly turns your whole app back into a client-rendered SPA and throws away most of the App Router's performance advantages.

Routing Is Nested, Not Flat

In the Pages Router, one file equals one route, full stop. The App Router changes this in a way that unlocks a lot of the framework's power but also takes some getting used to: routes are defined by nested folders, and each level of nesting can contribute its own layout.

app/
├── layout.tsx        → wraps everything
├── page.tsx           → route: /
├── dashboard/
│   ├── layout.tsx     → wraps everything under /dashboard
│   ├── page.tsx       → route: /dashboard
│   └── settings/
│       └── page.tsx   → route: /dashboard/settings

Navigating from /dashboard to /dashboard/settings re-renders only the settings/page.tsx content — the dashboard/layout.tsx around it (and the root layout around that) is preserved, including its React state and any data it already fetched. This is the payoff for using nested layouts instead of building your own persistent sidebar/header logic by hand: you get it for free, and it's fast because Next.js isn't re-fetching or re-rendering shared chrome on every navigation.

The Project Structure article covers the full set of file conventions — loading.tsx, error.tsx, not-found.tsx, route groups with parentheses like (marketing), dynamic segments with brackets like [slug] — but the takeaway for now is simpler: think of your app/ directory as a tree, where each folder is a URL segment and each layout.tsx along the path from the root to your page contributes shared UI.

Data Fetching Lives Where You Use It

In older React apps, and in the Pages Router, data fetching tends to live somewhere separate from where it's used — a getServerSideProps function at the top of a page file, or a useEffect hook that fires a request after the component mounts. The App Router collapses that distance. Because Server Components can be async, you fetch data directly inside the component that needs it:

// app/products/page.tsx
async function getProducts() {
  const res = await fetch("https://api.example.com/products");
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();

  return (
    <ul>
      {products.map((product: { id: string; name: string }) => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

There's no loading spinner logic here, no useEffect, no client-side request waterfall where the browser has to download your JavaScript before it even knows what data to ask for. The server does the fetching, resolves the promise, and streams fully-rendered HTML to the browser. If a child component further down the tree needs its own data, it fetches its own data — Next.js automatically deduplicates identical fetch calls made during the same render pass, so you don't need to thread data down through props just to avoid duplicate requests. The Fetching Data article covers streaming with loading.tsx and <Suspense> boundaries, which let you show a fast initial shell while slower data continues loading in the background — a pattern that's awkward to build by hand in a client-rendered app and nearly free here.

Mutations Are Also Just Functions

The App Router's answer to form submissions and data mutations is the Server Function (commonly called a Server Action when used from a form or event handler) — an async function marked with 'use server' that can be called directly from client code, with Next.js handling the network request under the hood:

// app/actions.ts
"use server";

export async function createTodo(formData: FormData) {
  const title = formData.get("title");
  await db.todos.create({ title });
}
// app/todos/page.tsx
import { createTodo } from "@/app/actions";

export default function TodosPage() {
  return (
    <form action={createTodo}>
      <input type="text" name="title" />
      <button type="submit">Add Todo</button>
    </form>
  );
}

No API route, no manually-written fetch call, no client-side JSON serialization boilerplate. The <form action={createTodo}> pattern works even without JavaScript enabled in the browser, because it degrades to a standard HTML form submission. The Mutating Data article covers progressive enhancement, useActionState for pending/error UI, and how mutations interact with the cache — which brings us to the next big concept.

Caching Is a Spectrum, Not a Switch

This is where App Router mental models diverge most sharply from what most developers expect. In the Pages Router, you chose a rendering strategy — static, server-rendered, or client-rendered — for an entire page, once. In the App Router, caching happens at multiple layers simultaneously, and understanding which layer you're dealing with will save you hours of confusion later:

  • The Data Cache — the extended fetch function can persist responses across requests and deployments, so identical requests don't hit your backend or database every time.
  • The Full Route Cache — Next.js can cache the rendered HTML and RSC payload for a route at build time, if nothing in that route opts into dynamic rendering.
  • The Router Cache (client-side) — the browser holds onto recently visited route segments so that back/forward navigation and repeated visits feel instant without a new server round-trip.

The Caching article covers each of these individually, and Revalidating covers how you invalidate them — either on a schedule (time-based revalidation) or on demand, right after a mutation, using revalidatePath or revalidateTag. If there's one thing worth internalizing now, it's this: a "cache miss" bug in the App Router is almost always a question of which layer is stale, not whether caching is happening at all. Newer versions of Next.js (16 and the cacheComponents flag in 15) push this further with explicit use cache directives, letting you opt individual functions and components into caching rather than relying on implicit rules — worth knowing exists even if the details belong in a dedicated article.

Handling Things Going Wrong

Real applications have two categories of "wrong": expected errors (a form validation failure, a 404 for a product that doesn't exist) and unexpected exceptions (a database connection drop, a bug). The App Router gives you distinct tools for each. Expected errors are typically returned as data — a { error: string } shape from a Server Action, checked and rendered conditionally in your JSX. Unexpected exceptions are caught automatically by the nearest error.tsx file in the route tree, which renders a fallback UI without crashing the rest of the page:

// app/dashboard/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong.</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Because error.tsx boundaries nest the same way layouts do, an error thrown deep in your route tree only takes out the closest boundary above it — not your entire application. The Error Handling article covers not-found.tsx, the notFound() function, and how this all composes with Suspense and streaming.

Styling, Images, and Fonts: The Batteries-Included Layer

A meaningful chunk of the App Router's value comes from problems it solves that have nothing to do with routing at all. The CSS article covers Tailwind CSS, CSS Modules, and global stylesheets — all supported without extra configuration, each with different scoping tradeoffs. The Image Optimization article covers the built-in next/image component, which automatically serves correctly-sized, modern-format images and prevents layout shift by requiring width and height up front. The Font Optimization article covers next/font, which self-hosts Google Fonts (or your own font files) at build time so the browser never makes a render-blocking request to an external font CDN.

None of these are "nice-to-haves" bolted onto a router — they exist because Core Web Vitals (the metrics Google uses for search ranking and that directly affect conversion rates) are disproportionately driven by image loading, layout shift, and font loading behavior. Baking the fix into the framework means you get good defaults without having to research image CDN configuration or font-display strategies yourself.

Talking to the Outside World: Metadata, Route Handlers, and Proxy

Three more pieces round out a typical full-stack app. Metadata and OG images covers the generateMetadata function and the Metadata API, which control what appears in a browser tab, in search results, and in social media link previews — including dynamically generated Open Graph images per page. Route Handlers covers route.ts files, the App Router's equivalent of a traditional REST endpoint, for the cases where you genuinely need to expose an HTTP API rather than a Server Function (webhooks from third-party services are the classic example, since they can't call a Server Function directly). Proxy covers the file that runs before a request is matched to a route — useful for authentication checks, redirects, and header rewriting at the edge, before any page-level code executes.

Shipping It: Deploying and Upgrading

The last two topics in this section, Deploying and Upgrading, are less about writing code and more about keeping an application alive. Deployment covers what a next build actually produces and how different hosting targets (a Node.js server, a static export, various platform adapters) consume that output differently — the choices you made earlier around caching and rendering directly affect which deployment targets are even viable for a given route. Upgrading covers how Next.js ships new major and minor versions, and the codemods the team provides to automate mechanical changes so you're not hand-editing every file in a large codebase when a convention changes.

Common Mistakes When You're Coming From Somewhere Else

Most of the confusion I've seen from developers picking up the App Router for the first time doesn't come from the App Router being hard — it comes from carrying assumptions over from wherever they learned React or Next.js originally. A few patterns show up over and over:

Marking everything 'use client' out of habit. If you learned React through Create React App or Vite, every component you've ever written has been a client component, so it's natural to reach for 'use client' reflexively. Do this at the top of your layout.tsx or a page that fetches data, and you lose server-side data fetching, automatic code splitting at the Server/Client boundary, and a meaningful chunk of the JavaScript-shipped-to-browser savings that were the whole point of switching routers. The fix isn't to avoid Client Components — interactivity requires them — it's to push the 'use client' boundary as far down the tree as possible, wrapping only the interactive leaf (a button, a form, a dropdown) rather than an entire page.

Fetching data in useEffect when a Server Component would do. If a component doesn't need to respond to user interaction after the initial render, there's rarely a good reason to fetch its data in the browser. A useEffect fetch means: ship JavaScript, wait for it to download and execute, then start the request — versus a Server Component, where the request starts before any JavaScript reaches the browser at all. This one shows up constantly in code ported from older React codebases, where the useEffect fetch pattern was the only option.

Expecting getStaticProps and getServerSideProps to exist. These are Pages Router APIs. They don't exist in app/, and there's no direct one-to-one replacement function — instead, the rendering strategy falls out of how you fetch data and whether anything in the route opts into dynamic behavior (reading cookies, headers, or search params, for instance). If you're migrating an existing Pages Router app, budget real time for this conceptual shift rather than looking for a mechanical find-and-replace.

Treating layouts as just "shared header and footer." Layouts do provide shared UI, but their more important property is that they preserve state and avoid re-fetching data across navigations within the same segment. A layout that fetches the current user once at the top of /dashboard doesn't refetch that data every time you navigate between /dashboard/settings and /dashboard/billing — only the page.tsx content inside changes. Missing this means you'll sometimes duplicate a fetch in every page that a shared layout could have handled once.

Assuming caching is either "on" or "off." As covered above, caching in the App Router happens at several independent layers. A page that looks "stuck" showing old data after a mutation is much more often a case of the wrong cache layer not being revalidated (forgetting to call revalidatePath or revalidateTag after a Server Function runs) than a framework bug. When something looks stale, the first question to ask is which of the three cache layers is actually responsible, not whether to disable caching altogether.

A Minimal End-to-End Example

It helps to see these pieces composed together, even briefly, before diving into individual articles. Here's a small route that touches routing, layouts, server-side data fetching, and a client-side interactive piece in one place:

// app/posts/layout.tsx
export default function PostsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <nav>
        <a href="/posts">All Posts</a>
      </nav>
      <main>{children}</main>
    </div>
  );
}
// app/posts/page.tsx
import { LikeButton } from "./like-button";

async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 60 },
  });
  return res.json();
}

export default async function PostsPage() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post: { id: string; title: string }) => (
        <li key={post.id}>
          {post.title}
          <LikeButton postId={post.id} />
        </li>
      ))}
    </ul>
  );
}
// app/posts/like-button.tsx
"use client";

import { useState } from "react";

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);

  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? "♥ Liked" : "♡ Like"}
    </button>
  );
}

Notice what's happening across these three files: the layout wraps every route under /posts and never re-renders on navigation between them. The page is an async Server Component that fetches data directly and tells Next.js to cache and revalidate that fetch every 60 seconds via the next: { revalidate: 60 } option — no external cache, no manual setTimeout. And the one piece of genuine interactivity, the like button, is isolated into its own file with 'use client', so the rest of the page — including the data fetch — stays server-only and ships no extra JavaScript for it. This is the shape most well-built App Router pages take: a server-rendered shell doing the data work, with small, deliberately-scoped client islands for anything that needs to respond to a click.

Why the Order of This Section Matters

If you read the sitemap of this documentation section top to bottom, the ordering isn't arbitrary. It roughly follows the lifecycle of building something real:

  1. Get a project running (Installation, Project Structure).
  2. Learn to build and connect pages (Layouts and Pages, Linking and Navigating).
  3. Understand where your code actually executes (Server and Client Components).
  4. Get data in and out (Fetching Data, Mutating Data).
  5. Make it fast and correct over time (Caching, Revalidating).
  6. Handle the unhappy paths (Error Handling).
  7. Polish the experience (CSS, Image Optimization, Font Optimization, Metadata and OG images).
  8. Extend beyond pages (Route Handlers, Proxy).
  9. Ship it, and keep it up to date (Deploying, Upgrading).

You don't have to read them in this order — plenty of developers jump straight to Fetching Data because that's the thing blocking them right now — but if you're building your first App Router project from scratch, following this sequence roughly matches the order you'll naturally hit each concern in a real project.

Key Takeaways

ConceptWhat It SolvesWhere It Lives
File-system routingTurns folder structure into URLs and nested layoutsapp/ directory conventions
Server Components (default)Runs code on the server, ships zero JS for non-interactive UIEvery file under app/ unless marked 'use client'
Client ComponentsEnables interactivity, hooks, browser APIsFiles with 'use client' at the top
async data fetching in componentsRemoves loading-spinner boilerplate and client waterfallsServer Components, colocated with the UI that needs the data
Server Functions ('use server')Handles mutations without hand-written API routesForms and event handlers, progressively enhanced
Multi-layer cachingAvoids redundant work across requests, builds, and navigationsData Cache, Full Route Cache, Router Cache
error.tsx / not-found.tsxContains failures to the nearest boundary instead of crashing the pageRoute segment files
next/image, next/fontSolves Core Web Vitals problems by defaultBuilt-in components
Route Handlers, ProxyExtends beyond page rendering into HTTP APIs and request interceptionroute.ts, proxy.ts

None of this replaces reading the individual articles that follow — each one goes deep enough to actually build with. But if you keep this map in your head as you read them, you'll spend a lot less time wondering why a feature exists or how it connects to the rest of the framework, and a lot more time just building.

Tags :
Share :

Related Posts

Can Next.js Be Used with GraphQL?

Can Next.js Be Used with GraphQL?

Next.js and GraphQL are two powerful technologies that have gained significant traction in the web development community. Next.js, a React-based fram

Dive Deeper
How does Next.js differ from Create React App?

How does Next.js differ from Create React App?

In the world of modern web development, React.js has emerged as a dominant force due to its flexibility, performance, and extensive ecosystem. Two po

Dive Deeper
How does Next.js handle image optimization?

How does Next.js handle image optimization?

In modern web development, image optimization plays a critical role in enhancing user experience and improving site performance. Large, unoptimized i

Dive Deeper