Getting started
Installation
npx create-next-app@latest my-app
cd my-app
npm run dev
Open http://localhost:3000 to see your app.
Project structure
app/
├── layout.tsx # Root layout (required)
├── page.tsx # Home page
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 UI
├── [slug]/
│ └── page.tsx # Dynamic route
├── (group)/ # Route group
│ └── page.tsx
├── @modal/ # Parallel route
│ └── page.tsx
└── api/
└── route.ts # API route
The app/ directory uses the App Router (recommended for new projects).
Routing patterns
| Pattern | Description |
|---|---|
[slug] |
Dynamic segment |
[...slug] |
Catch-all segments |
[[...slug]] |
Optional catch-all |
(group) |
Route group (no URL impact) |
@slot |
Parallel routes |
(.)folder |
Intercepting routes |
File-based routing automatically creates routes from your folder structure.
Pages & Layouts
Page component
// app/page.tsx - Server Component by default
export default async function Page() {
const data = await fetch("https://api.example.com/data");
const json = await data.json();
return <div>{json.title}</div>;
}
Server Components can use async/await directly.
Dynamic routes
// app/blog/[slug]/page.tsx
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <div>Post: {slug}</div>;
}
⚠️ In Next.js 15+, params is a Promise and must be awaited.
Layout component
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Layouts wrap pages and persist across navigations.
Loading UI
// app/loading.tsx
export default function Loading() {
return <div>Loading...</div>;
}
Automatically shown while page loads. Uses React Suspense.
Error UI
// app/error.tsx
"use client";
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Retry</button>
</div>
);
}
Error boundaries must be Client Components.
404 UI
// app/not-found.tsx
export default function NotFound() {
return <div>Page not found</div>;
}
Shown when notFound() is called or route doesn't exist.
Server & Client Components
Server Components
// No directive needed - default
export default async function ServerComponent() {
const data = await fetch("https://api.example.com");
const user = await db.users.find();
return <div>{/* content */}</div>;
}
- ✅ Direct database access
- ✅ Async/await anywhere
- ✅ Keep secrets on server
- ❌ No React hooks
- ❌ No browser APIs
- ❌ No event handlers
Client Components
"use client";
import { useState } from "react";
export default function ClientComponent() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
- ✅ React hooks
- ✅ Event handlers
- ✅ Browser APIs
- ❌ Direct database access
- ❌ Server-only modules
Use "use client" directive at the top of the file.
Composition pattern
// Server Component
import ClientComponent from "./ClientComponent";
export default async function Page() {
const data = await getData();
return (
<div>
<h1>Server content</h1>
<ClientComponent data={data} />
</div>
);
}
Pass server-fetched data as props to client components.
Data Fetching
Server-side fetch
// No caching by default (v15+)
const data = await fetch("https://api.example.com/data");
// With caching (opt-in)
const cached = await fetch(url, {
cache: "force-cache",
});
// No cache
const fresh = await fetch(url, {
cache: "no-store",
});
// Revalidation (ISR)
const revalidated = await fetch(url, {
next: { revalidate: 3600 },
});
⚠️ Fetch is NOT cached by default in Next.js 15+ (changed from v14).
Static generation
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
export default async function Page({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
return <div>Post: {slug}</div>;
}
Pre-renders pages at build time for dynamic routes.
Server Actions
// app/actions.ts
"use server";
export async function createTodo(formData: FormData) {
const title = formData.get("title");
await db.todos.create({ title });
}
// In component
import { createTodo } from "./actions";
export default function Form() {
return (
<form action={createTodo}>
<input name="title" />
<button type="submit">Add</button>
</form>
);
}
Functions that run on the server, callable from client or server.
Revalidation
import { revalidatePath, revalidateTag } from "next/cache";
// Revalidate specific path
revalidatePath("/blog");
// Revalidate by cache tag
revalidateTag("posts");
// Tag cache entries
fetch(url, {
next: { tags: ["posts"] },
});
Purge cached data on-demand.
Route Handlers
API routes
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const users = await db.users.findMany();
return NextResponse.json({ users });
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.users.create(body);
return NextResponse.json({ user }, { status: 201 });
}
Replace pages/api routes. Export HTTP method functions.
Dynamic routes
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const user = await db.users.find(id);
if (!user) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json({ user });
}
Access dynamic segments via params.
Middleware
// middleware.ts (project root)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: "/dashboard/:path*",
};
Runs before routes. Use for auth, redirects, rewrites.
Metadata & SEO
Static metadata
// app/page.tsx
export const metadata = {
title: "My App",
description: "App description",
keywords: ["next.js", "react"],
openGraph: {
title: "My App",
description: "App description",
images: ["/og-image.jpg"],
},
twitter: {
card: "summary_large_image",
title: "My App",
},
};
export default function Page() {
return <div>Content</div>;
}
Define metadata as an exported object.
Dynamic metadata
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
images: [post.image],
},
};
}
Async function for metadata based on route params or data.
Title template
// app/layout.tsx
export const metadata = {
title: {
template: "%s | My App",
default: "My App",
},
};
// app/about/page.tsx
export const metadata = {
title: "About", // Becomes "About | My App"
};
Template applies to all child pages.
Components & Hooks
Image component
import Image from 'next/image'
<Image
src="/photo.jpg"
alt="Photo"
width={500}
height={300}
/>
<Image
src="/hero.jpg"
alt="Hero"
fill
className="object-cover"
/>
<Image
src="https://example.com/photo.jpg"
alt="Remote"
width={500}
height={300}
/>
Automatic image optimization. Use fill for responsive images.
Link component
import Link from 'next/link'
<Link href="/about">About</Link>
<Link href="/blog/hello">
Post
</Link>
<Link
href="/blog"
prefetch={false}
>
Blog
</Link>
<Link
href={{
pathname: '/blog/[slug]',
query: { slug: 'hello' },
}}
>
Post
</Link>
Client-side navigation. Auto-prefetches in viewport.
Script component
import Script from 'next/script'
<Script
src="https://example.com/script.js"
strategy="lazyOnload"
/>
<Script id="inline-script">
{`console.log('Hello')`}
</Script>
Optimized script loading. Strategies: beforeInteractive, afterInteractive, lazyOnload.
useRouter
"use client";
import { useRouter } from "next/navigation";
export default function Component() {
const router = useRouter();
router.push("/dashboard");
router.replace("/login");
router.refresh();
router.back();
router.forward();
router.prefetch("/blog");
return <button onClick={() => router.push("/about")}>Go to About</button>;
}
Programmatic navigation. Client Component only.
usePathname
"use client";
import { usePathname } from "next/navigation";
export default function Nav() {
const pathname = usePathname();
return (
<nav>
<a href="/about" className={pathname === "/about" ? "active" : ""}>
About
</a>
</nav>
);
}
Get current pathname. Returns /about for /about?q=hello.
useSearchParams
"use client";
import { useSearchParams } from "next/navigation";
export default function Search() {
const searchParams = useSearchParams();
const query = searchParams.get("q");
return <div>Search: {query}</div>;
}
Access URL search params. Returns hello for ?q=hello.
Server-side helpers
import { redirect, notFound, cookies, headers } from "next/navigation";
// Redirect
redirect("/login");
// 404
notFound();
// Cookies
const cookieStore = await cookies();
const token = cookieStore.get("token");
// Headers
const headersList = await headers();
const userAgent = headersList.get("user-agent");
⚠️ cookies() and headers() are async in Next.js 15+.
Configuration
next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**.example.com",
},
],
},
async redirects() {
return [
{
source: "/old",
destination: "/new",
permanent: true,
},
];
},
async rewrites() {
return [
{
source: "/api/:path*",
destination: "https://api.example.com/:path*",
},
];
},
};
export default nextConfig;
Configuration for images, redirects, rewrites, and more.
Environment variables
# .env.local (gitignored)
DATABASE_URL=postgresql://...
API_SECRET=secret123
# Public variables (exposed to browser)
NEXT_PUBLIC_API_URL=https://api.example.com
// Server Component
const secret = process.env.API_SECRET;
// Client Component
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
Use NEXT_PUBLIC_ prefix for browser-exposed variables.
Route segment config
// app/page.tsx or app/layout.tsx
export const dynamic = "force-dynamic"; // or 'force-static'
export const revalidate = 3600; // seconds
export const runtime = "edge"; // or 'nodejs'
export const preferredRegion = "auto"; // or ['iad1', 'sfo1']
export const fetchCache = "force-cache";
Configure route behavior per segment.
Build & deployment
# Development
npm run dev
# Production build
npm run build
# Start production server
npm run start
# Static export
npm run build && npm run export
// next.config.ts
const nextConfig = {
output: "export", // Static HTML export
};
For static export, set output: 'export' in config.
Important Gotchas
Async params (v15+)
// ❌ Wrong (v14 style)
export default function Page({ params }) {
const { slug } = params;
return <div>{slug}</div>;
}
// ✅ Correct (v15+)
export default async function Page({ params }) {
const { slug } = await params;
return <div>{slug}</div>;
}
params and searchParams are Promises in Next.js 15+.
Async cookies/headers
// ❌ Wrong (v14 style)
const token = cookies().get("token");
// ✅ Correct (v15+)
const cookieStore = await cookies();
const token = cookieStore.get("token");
cookies() and headers() are now async.
Fetch caching
// ❌ Cached by default (v14)
// ✅ NOT cached by default (v15+)
const data = await fetch("https://api.example.com");
// Opt-in to caching
const cached = await fetch(url, { cache: "force-cache" });
Fetch behavior changed in v15. No longer cached by default.
Client boundaries
// ❌ Wrong - entire tree becomes client
"use client";
import ServerComponent from "./ServerComponent";
// ✅ Correct - isolate client component
import ClientComponent from "./ClientComponent";
export default async function Page() {
return (
<div>
<ServerComponent />
<ClientComponent />
</div>
);
}
"use client" makes ALL imports part of client bundle.
Route conflicts
app/
├── dashboard/
│ ├── page.tsx # ✅ OK
│ └── route.ts # ❌ Conflict!
page.tsx and route.ts cannot coexist in the same folder.
Also see
- Next.js Documentation - Official docs
- Next.js GitHub - Source code & issues
- Next.js Examples - Official examples
- App Router Playground - Interactive examples
- React Documentation - React fundamentals