Next.js — Getting Started
Next.js is a production-grade React framework built by Vercel. It extends React with server-side rendering, static generation, file-based routing, and a rich set of conventions for building full-stack web applications. Unlike plain React (which is client-only), Next.js renders components on the server by default, sending fully formed HTML to the browser for faster initial loads and better SEO.
Since version 13, Next.js introduced the App Router — a new routing paradigm built on React Server Components (RSC). The App Router provides co-located data fetching, streaming, nested layouts, loading states, error boundaries, and parallel routes. This guide covers everything you need to build production applications with the App Router.
info
The fastest way to start a new Next.js project is with create-next-app. It sets up TypeScript, ESLint, Tailwind CSS, and the App Router by default.
| 1 | # Interactive setup (recommended) |
| 2 | npx create-next-app@latest my-app |
| 3 | |
| 4 | # With specific options |
| 5 | npx create-next-app@latest my-app \ |
| 6 | --typescript \ |
| 7 | --tailwind \ |
| 8 | --eslint \ |
| 9 | --app \ |
| 10 | --src-dir \ |
| 11 | --import-alias "@/*" |
After installation, the project structure looks like this:
| 1 | my-app/ |
| 2 | ├── src/ |
| 3 | │ └── app/ |
| 4 | │ ├── layout.tsx # Root layout (required) |
| 5 | │ ├── page.tsx # Home page (/) |
| 6 | │ ├── globals.css # Global styles |
| 7 | │ └── favicon.ico # Favicon |
| 8 | ├── public/ # Static assets |
| 9 | ├── next.config.ts # Next.js configuration |
| 10 | ├── tailwind.config.ts # Tailwind configuration |
| 11 | ├── tsconfig.json # TypeScript configuration |
| 12 | ├── package.json |
| 13 | └── .env.local # Environment variables |
The App Router uses the app/ directory as its root. Every folder inside app/ becomes a route. Special files like page.tsx, layout.tsx, loading.tsx, error.tsx, and not-found.tsx define the UI for each route.
| 1 | app/ |
| 2 | ├── layout.tsx # Root layout — wraps all pages |
| 3 | ├── page.tsx # Home page (/) |
| 4 | ├── loading.tsx # Loading UI for / |
| 5 | ├── error.tsx # Error boundary for / |
| 6 | ├── not-found.tsx # 404 page for / |
| 7 | ├── globals.css # Global styles |
| 8 | │ |
| 9 | ├── about/ |
| 10 | │ └── page.tsx # /about |
| 11 | │ |
| 12 | ├── blog/ |
| 13 | │ ├── layout.tsx # Shared layout for /blog/* |
| 14 | │ ├── page.tsx # /blog |
| 15 | │ ├── loading.tsx # Loading UI for /blog |
| 16 | │ ├── [slug]/ |
| 17 | │ │ └── page.tsx # /blog/:slug |
| 18 | │ └── tag/ |
| 19 | │ └── [tag]/ |
| 20 | │ └── page.tsx # /blog/tag/:tag |
| 21 | │ |
| 22 | ├── dashboard/ |
| 23 | │ ├── layout.tsx # Dashboard layout |
| 24 | │ ├── page.tsx # /dashboard |
| 25 | │ ├── settings/ |
| 26 | │ │ └── page.tsx # /dashboard/settings |
| 27 | │ └── analytics/ |
| 28 | │ └── page.tsx # /dashboard/analytics |
| 29 | │ |
| 30 | ├── api/ |
| 31 | │ └── users/ |
| 32 | │ └── route.ts # /api/users (API route) |
| 33 | │ |
| 34 | └── (marketing)/ |
| 35 | ├── layout.tsx # Marketing layout group |
| 36 | ├── pricing/ |
| 37 | │ └── page.tsx # /pricing (no URL segment) |
| 38 | └── features/ |
| 39 | └── page.tsx # /features |
info
Next.js is built on several key concepts that differentiate it from plain React. Understanding these concepts is essential for building efficient applications.
React Server Components (RSC)
Components that render on the server. They can directly access databases, file systems, and APIs without creating API endpoints. They send zero JavaScript to the client, reducing bundle size dramatically.
File-Based Routing
Routes are defined by the file system. Each folder becomes a URL segment, and special files (page.tsx, layout.tsx) define the UI.
Nested Layouts
Layouts wrap child routes without remounting. They persist across navigations and allow shared UI to remain interactive while child content changes.
Streaming & Suspense
The server can send HTML progressively. Slow components stream in as they resolve while fast components render immediately, reducing Time to First Byte (TTFB).
Server Actions
Async functions that run on the server and can be called directly from client components. They eliminate the need for manual API routes for form submissions and data mutations.
A page is a React component that renders at a specific URL. Create a page.tsx file inside the app/ directory. By default, pages are React Server Components — they run only on the server.
| 1 | // app/page.tsx — Home page (/) |
| 2 | export default function HomePage() { |
| 3 | return ( |
| 4 | <main> |
| 5 | <h1>Welcome to Next.js</h1> |
| 6 | <p>This page is rendered on the server.</p> |
| 7 | </main> |
| 8 | ); |
| 9 | } |
To add interactivity (event handlers, state, effects), add the 'use client' directive at the top of the file. This marks the component as a Client Component that runs in the browser.
| 1 | "use client"; |
| 2 | |
| 3 | import { useState } from "react"; |
| 4 | |
| 5 | export default function CounterPage() { |
| 6 | const [count, setCount] = useState(0); |
| 7 | |
| 8 | return ( |
| 9 | <main> |
| 10 | <h1>Counter</h1> |
| 11 | <p>Count: {count}</p> |
| 12 | <button onClick={() => setCount(count + 1)}> |
| 13 | Increment |
| 14 | </button> |
| 15 | </main> |
| 16 | ); |
| 17 | } |
info
Layouts are special files that wrap pages. They persist across navigations — when a user moves between pages, the layout does not remount. This is perfect for shared UI like navigation bars, sidebars, and footers.
| 1 | // app/layout.tsx — Root layout (required) |
| 2 | import type { Metadata } from "next"; |
| 3 | import "./globals.css"; |
| 4 | |
| 5 | export const metadata: Metadata = { |
| 6 | title: "My App", |
| 7 | description: "Built with Next.js", |
| 8 | }; |
| 9 | |
| 10 | export default function RootLayout({ |
| 11 | children, |
| 12 | }: { |
| 13 | children: React.ReactNode; |
| 14 | }) { |
| 15 | return ( |
| 16 | <html lang="en"> |
| 17 | <body> |
| 18 | <nav> |
| 19 | <a href="/">Home</a> |
| 20 | <a href="/about">About</a> |
| 21 | <a href="/blog">Blog</a> |
| 22 | </nav> |
| 23 | <main>{children}</main> |
| 24 | <footer>© 2026 My App</footer> |
| 25 | </body> |
| 26 | </html> |
| 27 | ); |
| 28 | } |
| 1 | // app/dashboard/layout.tsx — Nested layout |
| 2 | import { Sidebar } from "@/components/sidebar"; |
| 3 | |
| 4 | export default function DashboardLayout({ |
| 5 | children, |
| 6 | }: { |
| 7 | children: React.ReactNode; |
| 8 | }) { |
| 9 | return ( |
| 10 | <div className="flex"> |
| 11 | <Sidebar /> |
| 12 | <div className="flex-1 p-8">{children}</div> |
| 13 | </div> |
| 14 | ); |
| 15 | } |
warning
Next.js provides special files for handling loading and error states. These integrate with React Suspense and Error Boundaries automatically.
| 1 | // app/loading.tsx — Shown while the page is loading |
| 2 | export default function Loading() { |
| 3 | return ( |
| 4 | <div className="animate-pulse"> |
| 5 | <div className="h-8 bg-gray-200 rounded w-1/3 mb-4" /> |
| 6 | <div className="h-4 bg-gray-200 rounded w-full mb-2" /> |
| 7 | <div className="h-4 bg-gray-200 rounded w-2/3" /> |
| 8 | </div> |
| 9 | ); |
| 10 | } |
| 1 | // app/error.tsx — Error boundary for this route segment |
| 2 | "use client"; |
| 3 | |
| 4 | import { useEffect } from "react"; |
| 5 | |
| 6 | export default function Error({ |
| 7 | error, |
| 8 | reset, |
| 9 | }: { |
| 10 | error: Error & { digest?: string }; |
| 11 | reset: () => void; |
| 12 | }) { |
| 13 | useEffect(() => { |
| 14 | console.error(error); |
| 15 | }, [error]); |
| 16 | |
| 17 | return ( |
| 18 | <div className="text-center py-20"> |
| 19 | <h2>Something went wrong!</h2> |
| 20 | <p>{error.message}</p> |
| 21 | <button onClick={() => reset()}>Try again</button> |
| 22 | </div> |
| 23 | ); |
| 24 | } |
| 1 | // app/not-found.tsx — 404 page |
| 2 | import Link from "next/link"; |
| 3 | |
| 4 | export default function NotFound() { |
| 5 | return ( |
| 6 | <div className="text-center py-20"> |
| 7 | <h2>404 — Page Not Found</h2> |
| 8 | <p>The page you are looking for does not exist.</p> |
| 9 | <Link href="/">Go back home</Link> |
| 10 | </div> |
| 11 | ); |
| 12 | } |
info
Next.js can render pages at build time (static) or at request time (dynamic). By default, pages without dynamic dependencies are statically generated during the build. Pages that use dynamic functions like cookies(), headers(), or searchParams are automatically rendered on the server at request time.
| 1 | // Static page — built at build time |
| 2 | // No dynamic APIs used, so this is static by default |
| 3 | export default function AboutPage() { |
| 4 | return <h1>About Us</h1>; |
| 5 | } |
| 6 | |
| 7 | // Dynamic page — rendered at request time |
| 8 | // Using cookies() forces dynamic rendering |
| 9 | import { cookies } from "next/headers"; |
| 10 | |
| 11 | export default async function DashboardPage() { |
| 12 | const cookieStore = await cookies(); |
| 13 | const session = cookieStore.get("session"); |
| 14 | |
| 15 | if (!session) { |
| 16 | return <p>Please log in</p>; |
| 17 | } |
| 18 | |
| 19 | return <h1>Welcome, {session.value}</h1>; |
| 20 | } |
You can also explicitly force dynamic rendering with export const dynamic = "force-dynamic" or set revalidation intervals for ISR (Incremental Static Regeneration).
| 1 | // Force dynamic rendering for this route |
| 2 | export const dynamic = "force-dynamic"; |
| 3 | |
| 4 | // Or set revalidation for ISR |
| 5 | export const revalidate = 60; // Revalidate every 60 seconds |
| 6 | |
| 7 | // Or force static with on-demand revalidation |
| 8 | export const revalidate = false; // Static, never revalidate automatically |
info
Next.js provides a comprehensive metadata API for SEO. You can define metadata statically with the metadata export or dynamically with the generateMetadata function.
| 1 | // Static metadata |
| 2 | import type { Metadata } from "next"; |
| 3 | |
| 4 | export const metadata: Metadata = { |
| 5 | title: "My Page Title", |
| 6 | description: "Page description for search engines", |
| 7 | openGraph: { |
| 8 | title: "My Page Title", |
| 9 | description: "What shows up on social media", |
| 10 | images: ["/og-image.png"], |
| 11 | }, |
| 12 | twitter: { |
| 13 | card: "summary_large_image", |
| 14 | title: "My Page Title", |
| 15 | description: "Twitter card description", |
| 16 | }, |
| 17 | }; |
| 18 | |
| 19 | // Dynamic metadata |
| 20 | export async function generateMetadata({ |
| 21 | params, |
| 22 | }: { |
| 23 | params: Promise<{ slug: string }>; |
| 24 | }): Promise<Metadata> { |
| 25 | const { slug } = await params; |
| 26 | const post = await getPost(slug); |
| 27 | |
| 28 | return { |
| 29 | title: post.title, |
| 30 | description: post.excerpt, |
| 31 | openGraph: { |
| 32 | title: post.title, |
| 33 | description: post.excerpt, |
| 34 | images: [post.coverImage], |
| 35 | }, |
| 36 | }; |
| 37 | } |
best practice
The App Router supports several advanced routing patterns beyond basic file-based routes.
Dynamic Routes
| 1 | app/blog/[slug]/page.tsx → /blog/:slug |
| 2 | app/shop/[...slug]/page.tsx → /shop/* (catch-all) |
| 3 | app/shop/[[...slug]]/page.tsx → /shop/* (optional catch-all) |
| 4 | app/(group)/page.tsx → / (route group, no URL segment) |
Parallel Routes
Render multiple pages in the same layout simultaneously using named slots. Useful for dashboards, modals, and split views.
| 1 | // app/dashboard/layout.tsx |
| 2 | export default function Layout({ |
| 3 | children, |
| 4 | analytics, |
| 5 | team, |
| 6 | }: { |
| 7 | children: React.ReactNode; |
| 8 | analytics: React.ReactNode; |
| 9 | team: React.ReactNode; |
| 10 | }) { |
| 11 | return ( |
| 12 | <div className="grid grid-cols-2 gap-4"> |
| 13 | <div>{children}</div> |
| 14 | <div>{analytics}</div> |
| 15 | <div>{team}</div> |
| 16 | </div> |
| 17 | ); |
| 18 | } |
Intercepting Routes
Intercept a route as if the user navigated to it, but keep the current context visible. Use the (...) syntax.
| 1 | app/feed/[id]/page.tsx → /feed/123 (full page) |
| 2 | app/feed/(..)photo/[id]/page.tsx → intercepts /photo/123 as modal |
Middleware runs before a request is completed. It can modify the request, rewrite URLs, set cookies, redirect, or return early. Place a middleware.ts file at the root of your project (or in src/).
| 1 | // middleware.ts (project root) |
| 2 | import { NextResponse } from "next/server"; |
| 3 | import type { NextRequest } from "next/server"; |
| 4 | |
| 5 | export function middleware(request: NextRequest) { |
| 6 | // Check authentication |
| 7 | const token = request.cookies.get("session")?.value; |
| 8 | |
| 9 | if (!token && request.nextUrl.pathname.startsWith("/dashboard")) { |
| 10 | return NextResponse.redirect(new URL("/login", request.url)); |
| 11 | } |
| 12 | |
| 13 | // Add custom headers |
| 14 | const response = NextResponse.next(); |
| 15 | response.headers.set("x-request-id", crypto.randomUUID()); |
| 16 | return response; |
| 17 | } |
| 18 | |
| 19 | export const config = { |
| 20 | matcher: [ |
| 21 | // Match all paths except static files and images |
| 22 | "/((?!_next/static|_next/image|favicon.ico|public/).*)", |
| 23 | ], |
| 24 | }; |
info
Next.js supports environment variables out of the box. Variables prefixed with NEXT_PUBLIC_ are exposed to the browser. All other variables are server-only.
| 1 | # .env.local (never committed to git) |
| 2 | DATABASE_URL=postgresql://user:pass@localhost:5432/mydb |
| 3 | API_SECRET=sk-secret-key |
| 4 | |
| 5 | # Available on both server and client |
| 6 | NEXT_PUBLIC_API_URL=https://api.example.com |
| 7 | NEXT_PUBLIC_SITE_NAME=My App |
| 1 | // Server Component — can access all env vars |
| 2 | export default async function ServerPage() { |
| 3 | const dbUrl = process.env.DATABASE_URL; // ✅ Available |
| 4 | const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ Available |
| 5 | |
| 6 | return <p>Server-only content</p>; |
| 7 | } |
| 1 | // Client Component — only NEXT_PUBLIC_ vars |
| 2 | "use client"; |
| 3 | |
| 4 | export default function ClientPage() { |
| 5 | const apiUrl = process.env.NEXT_PUBLIC_API_URL; // ✅ Available |
| 6 | const dbUrl = process.env.DATABASE_URL; // ❌ undefined |
| 7 | |
| 8 | return <p>Client content: {apiUrl}</p>; |
| 9 | } |
warning
| File | Purpose | Required |
|---|---|---|
| page.tsx | Defines the UI for a route | Yes (per route) |
| layout.tsx | Shared UI that wraps child routes | Root only |
| loading.tsx | Loading UI (Suspense boundary) | No |
| error.tsx | Error boundary | No |
| not-found.tsx | 404 UI | No |
| route.ts | API endpoint (Route Handler) | No |
| template.tsx | Like layout but remounts on navigation | No |
| default.tsx | Fallback for parallel routes | No |
best practice