App Router, Server Components, Data Fetching, API Routes, Server Actions, Middleware, Deployment — full-stack React framework.
// ── Root Layout (required) ──
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}// ── Home Page (Server Component by default) ──
export default function HomePage() {
return (
<main>
<h1>Welcome to Next.js 16</h1>
<p>This is a Server Component.</p>
</main>
);
}
// ── Page metadata (SEO) ──
export const metadata: Metadata = {
title: 'My App',
description: 'Built with Next.js 16',
openGraph: {
title: 'My App',
description: 'Built with Next.js 16',
images: ['/og-image.png'],
},
};
}// ── Dynamic Route ──
interface BlogPostPageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ page?: string }>;
}
export default async function BlogPostPage({ params, searchParams }: BlogPostPageProps) {
const { slug } = await params;
const { page } = await searchParams;
const post = await getPost(slug); // Fetch data in Server Component
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}| File | Purpose |
|---|---|
| layout.tsx | Shared UI that wraps children (root or nested) |
| page.tsx | Unique UI for a route (makes route publicly accessible) |
| loading.tsx | Instant loading UI (Suspense fallback) |
| error.tsx | Error boundary for unexpected errors |
| not-found.tsx | 404 UI (triggered via notFound()) |
| default.tsx | Fallback UI for parallel routes |
| template.tsx | Re-rendered layout (unlike layout, re-mounts) |
| route.ts | API endpoint (GET, POST, etc.) |
| middleware.ts | Request interceptor (auth, redirects) |
| Export | Description |
|---|---|
| metadata | Static route metadata (title, description) |
| generateMetadata | Dynamic metadata (async function) |
| generateStaticParams | Pre-render dynamic routes at build |
| dynamicParams | Allow non-generated dynamic routes |
| revalidate | ISR revalidation interval (seconds) |
| runtime | "edge" or "nodejs" per route |
| maxDuration | Max execution time (serverless) |
| preferredRegion | Preferred deployment region |
// ── Nested Layout ──
export default function DashboardLayout({
children,
sidebar,
notifications,
}: {
children: React.ReactNode;
sidebar: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="dashboard">
<aside>{sidebar}</aside>
<main>{children}</main>
<aside>{notifications}</aside>
</div>
);
}app/ directory uses React Server Components by default, supports layouts, error boundaries, loading states, and streaming out of the box. The legacy pages/ router is still supported but not recommended for new projects.# ── Route Organization ──
src/app/
├── page.tsx # / (home)
├── layout.tsx # root layout
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog (list)
│ ├── [slug]/
│ │ └── page.tsx # /blog/:slug (detail)
│ └── layout.tsx # /blog layout
├── shop/
│ ├── @products/
│ │ ├── page.tsx # Parallel route slot
│ │ └── loading.tsx
│ ├── @cart/
│ │ └── default.tsx # Fallback for parallel route
│ ├── layout.tsx # /shop layout with slots
│ └── page.tsx # /shop
├── (auth)/ # Route group (no URL impact)
│ ├── login/
│ │ └── page.tsx # /login
│ └── register/
│ └── page.tsx # /register
├── api/
│ └── users/
│ └── route.ts # /api/users (API handler)
└── [[...slug]]/
└── page.tsx # Catch-all (optional) for 404/custom pages// ── Route Group Layout ──
// (auth) group: no URL segment, shared layout for login/register
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<div className="w-full max-w-md">{children}</div>
</div>
);
}// ── Parallel Routes Layout ──
export default function ShopLayout({
children,
products,
cart,
}: {
children: React.ReactNode;
products: React.ReactNode;
cart: React.ReactNode;
}) {
return (
<div className="flex">
<nav>{products}</nav>
<main>{children}</main>
<aside>{cart}</aside>
</div>
);
}// ── Catch-All Segment ──
// /photos/1 → { slug: ['1'] }
// /photos/1/2/3 → { slug: ['1', '2', '3'] }
// /photos → no match (use [[...slug]] for optional)
interface PhotosPageProps {
params: Promise<{ slug: string[] }>;
}
export default async function PhotosPage({ params }: PhotosPageProps) {
const { slug } = await params;
// slug is an array of segments
return <div>{'Photo path: '}{slug.join(' / ')}</div>;
}| Pattern | Example | Matches |
|---|---|---|
| Static | /about | /about only |
| Dynamic | /blog/[slug] | /blog/hello-world |
| Catch-All | /shop/[...slug] | /shop/a, /shop/a/b/c |
| Optional Catch-All | /[[...slug]] | /, /a, /a/b/c |
| Route Group | (auth)/login | /login (no URL segment) |
| Parallel | @panel/page.tsx | Named slot in layout |
import Link from 'next/link';
import { useRouter, usePathname } from 'next/navigation';
function Nav() {
const router = useRouter();
const pathname = usePathname();
return (
<nav>
<Link href="/about">About</Link>
<Link href={'/blog/' + post.slug}>
Read More
</Link>
{/* Programmatic navigation */}
<button onClick={() => router.push('/dashboard')}>
Dashboard
</button>
<button onClick={() => router.back()}>
Go Back
</button>
<button onClick={() => router.refresh()}>
Refresh
</button>
</nav>
);
}next/link for client-side navigation instead of <a> tags. Next.js Link prefetches pages in the background, providing instant navigation. For programmatic navigation, use useRouter() from next/navigation.// ── Server Component (no 'use client') ──
// Runs on the server only. No useState, useEffect, event handlers.
import { db } from '@/lib/db';
// async Server Components can directly fetch data
export default async function UsersPage() {
const users = await db.user.findMany();
return (
<div>
<h1>Users</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name} - {user.email}</li>
))}
</ul>
</div>
);
}// ── Client Component ──
'use client';
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
// useEffect, event handlers, hooks all work here
useEffect(() => {
console.log('Count changed:', count);
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Increment</button>
</div>
);
}// ── Composition: Server + Client ──
// Server Component wraps Client Component
import ClientSearch from './ClientSearch';
import ClientLikeButton from './ClientLikeButton';
// This file is a Server Component (no 'use client')
export default async function ProductsPage() {
const products = await db.product.findMany();
return (
<div>
<h1>Products</h1>
{/* Client component for interactivity */}
<ClientSearch placeholder="Search products..." />
<div className="grid">
{products.map(product => (
<div key={product.id}>
<h2>{product.name}</h2>
<p>{product.description}</p>
{/* Pass server-fetched data as props */}
<ClientLikeButton productId={product.id} />
</div>
))}
</div>
</div>
);
}| Feature | Server Component | Client Component |
|---|---|---|
| Default | Yes (no directive needed) | Needs 'use client' |
| Fetch data | Direct async/await | Use SWR/React Query |
| Access backend | Yes (DB, FS, env vars) | No direct access |
| useState/useEffect | No | Yes |
| Event handlers | No (onClick, onChange) | Yes |
| Browser APIs | No | Yes |
| Bundle size | Zero JS sent to client | JS included in bundle |
| SEO | Full HTML rendered | Hydrated on client |
| use hooks | Limited (no state hooks) | All React hooks |
'use client', extract the interactive part into a child Client Component and keep the parent as Server Component.'use client' when you need interactivity (state, effects, event handlers, browser APIs).// ── Direct Fetch in Server Components ──
export default async function DashboardPage() {
// These are cached & deduped automatically
const user = await fetch('https://api.example.com/user').then(r => r.json());
// With caching options
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 }, // ISR: revalidate every hour
}).then(r => r.json());
// No cache (always fresh)
const stats = await fetch('https://api.example.com/stats', {
cache: 'no-store',
}).then(r => r.json());
return <div>{/* render data */}</div>;
}// ── Incremental Static Regeneration (ISR) ──
// Revalidate page every 60 seconds
export const revalidate = 60;
// Revalidate on demand (triggered by API or webhook)
export const revalidate = 0; // Default: on-demand
// Per-fetch ISR
const data = await fetch(url, { next: { revalidate: 3600 } });
// ── On-Demand Revalidation ──
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest) {
const { path, tag } = await request.json();
// Revalidate a specific path
revalidatePath('/blog');
// Revalidate all pages with a specific cache tag
revalidateTag('blog-posts');
return Response.json({ revalidated: true, now: Date.now() });
}// ── Client Component with SWR ──
'use client';
import useSWR from 'swr';
const fetcher = (url: string) => fetch(url).then(r => r.json());
export default function UserProfile({ userId }: { userId: string }) {
const { data, error, isLoading } = useSWR(
'/api/users/' + userId,
fetcher,
{
revalidateOnFocus: false,
revalidateIfStale: true,
}
);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading user</div>;
return <div>{data.name}</div>;
}// ── Streaming ──
import { Suspense } from 'react';
function PostsFeed() {
// This component streams in progressively
return <div>{/* async content */}</div>;
}
function Analytics() {
// Slow component doesn't block fast content
return <div>{/* analytics data */}</div>;
}
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
{/* Shows loading.tsx while PostsFeed loads */}
<Suspense fallback={<PostsSkeleton />}>
<PostsFeed />
</Suspense>
{/* Analytics loads independently */}
<Suspense fallback={<AnalyticsSkeleton />}>
<Analytics />
</Suspense>
</div>
);
}| Strategy | Config | Use Case |
|---|---|---|
| Static (default) | fetch() + default | Build-time, rarely changes |
| ISR | revalidate: 60 | Periodic revalidation |
| On-Demand | revalidatePath/Tag | Revalidate on update |
| No Cache | cache: "no-store" | Real-time data |
| Per Request | requestStore | User-specific data |
| Pattern | Location | Best For |
|---|---|---|
| Server Component fetch | page.tsx (async) | Static/ISR pages |
| Server Actions | action.ts (server) | Form submissions, mutations |
| Route Handlers | route.ts (GET) | REST API endpoints |
| SWR (client) | Client component | Client-side polling |
| React Query | Client component | Complex client caching |
fetch() calls in a Server Component tree are automatically deduplicated into one request. Use cache: 'no-store' for real-time data or revalidate for ISR.// ── Route Handlers (REST API) ──
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
// GET /api/users
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const users = await db.user.findMany({
skip: (page - 1) * limit,
take: limit,
});
return NextResponse.json({ users, page, limit });
}
// POST /api/users
export async function POST(request: NextRequest) {
const body = await request.json();
if (!body.name || !body.email) {
return NextResponse.json(
{ error: 'Name and email are required' },
{ status: 400 }
);
}
const user = await db.user.create({
data: { name: body.name, email: body.email },
});
return NextResponse.json(user, { status: 201 });
}// ── Dynamic API Route ──
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
// GET /api/users/:id
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id } });
if (!user) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
}
return NextResponse.json(user);
}
// PUT /api/users/:id
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const body = await request.json();
const user = await db.user.update({
where: { id },
data: body,
});
return NextResponse.json(user);
}
// DELETE /api/users/:id
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await db.user.delete({ where: { id } });
return NextResponse.json({ success: true });
}// ── Server Actions ──
// src/app/actions.ts
'use server';
import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
});
export async function createPost(formData: FormData) {
const validated = CreatePostSchema.parse({
title: formData.get('title'),
content: formData.get('content'),
});
await db.post.create({
data: {
title: validated.title,
content: validated.content,
authorId: 'current-user-id', // Get from auth
},
});
revalidatePath('/blog'); // Revalidate the blog page
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } });
revalidatePath('/blog');
}// ── Form with Server Action ──
import { createPost } from '@/app/actions';
export default function NewPostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" required />
<textarea name="content" placeholder="Content..." required />
<button type="submit">Create Post</button>
</form>
);
}
// ── With useActionState (React 19) ──
'use client';
import { useActionState } from 'react';
import { createPost } from '@/app/actions';
function PostForm() {
const [state, formAction, isPending] = useActionState(
async (prevState: any, formData: FormData) => {
const result = await createPost(formData);
return { success: true };
},
null
);
return (
<form action={formAction}>
<input name="title" required />
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
);
}| Export | HTTP Method | Use Case |
|---|---|---|
| GET | GET | Read / fetch data |
| POST | POST | Create resource |
| PUT | PUT | Full update |
| PATCH | PATCH | Partial update |
| DELETE | DELETE | Remove resource |
| HEAD | HEAD | Headers only |
| OPTIONS | OPTIONS | CORS preflight |
useActionState for loading states and error handling.// ── Authentication Middleware ──
import { NextRequest, NextResponse } from 'next/server';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Public routes that don't require auth
const publicPaths = ['/login', '/register', '/api/auth'];
if (publicPaths.some(path => pathname.startsWith(path))) {
return NextResponse.next();
}
// Check for auth token
const token = request.cookies.get('auth-token')?.value;
if (!token) {
const loginUrl = new URL('/login', request.url);
loginUrl.searchParams.set('callbackUrl', pathname);
return NextResponse.redirect(loginUrl);
}
// Add user info to headers for downstream use
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', 'decoded-user-id');
return NextResponse.next({
request: { headers: requestHeaders },
});
}
// ── Matcher: only run on specific paths ──
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|public/).*)',
],
};// ── Rate Limiting Middleware ──
import { NextRequest, NextResponse } from 'next/server';
const rateLimitMap = new Map<string, { count: number; lastReset: number }>();
const RATE_LIMIT = 100; // requests per window
const WINDOW_MS = 60 * 1000; // 1 minute
export function middleware(request: NextRequest) {
const ip = request.ip || 'unknown';
const now = Date.now();
const record = rateLimitMap.get(ip);
if (!record || now - record.lastReset > WINDOW_MS) {
rateLimitMap.set(ip, { count: 1, lastReset: now });
return NextResponse.next();
}
if (record.count >= RATE_LIMIT) {
return NextResponse.json(
{ error: 'Too many requests' },
{ status: 429, headers: { 'Retry-After': '60' } }
);
}
record.count++;
return NextResponse.next();
}// ── Locale / i18n Redirect Middleware ──
import { NextRequest, NextResponse } from 'next/server';
const locales = ['en', 'fr', 'de', 'es'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Skip if already has locale prefix or is static asset
if (
pathname.startsWith('/_next') ||
pathname.startsWith('/api') ||
locales.some(locale => pathname.startsWith('/' + locale))
) {
return NextResponse.next();
}
// Check Accept-Language header or cookie
const acceptLang = request.headers.get('accept-language') || '';
const preferred = locales.find(l => acceptLang.includes(l)) || defaultLocale;
// Redirect to localized URL
const url = request.nextUrl.clone();
url.pathname = '/' + preferred + pathname;
return NextResponse.redirect(url);
}
export const config = {
matcher: ['/((?!_next|api|favicon.ico).*)'],
};| Property/Method | Description |
|---|---|
| request.nextUrl | Parsed URL (NextURL object) |
| request.cookies | Cookie access (get/set/delete) |
| request.ip | Client IP address |
| request.geo | Geolocation data (Vercel) |
| NextResponse.next() | Continue to route |
| NextResponse.redirect() | Redirect to URL |
| NextResponse.rewrite() | Rewrite URL (keep original) |
| NextResponse.json() | Return JSON response |
export const config = {
matcher: [
'/dashboard/:path*',
'/api/:path*',
'/((?!static|_next).*)',
],
};fs, path, or native modules. For JWT verification, use the jose library which is Edge-compatible.// ── Next.js Configuration ──
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// Image domains
images: {
remotePatterns: [
{ protocol: 'https', hostname: '**.example.com' },
],
formats: ['image/avif', 'image/webp'],
},
// Headers for all routes
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
],
},
];
},
// Redirects
async redirects() {
return [
{ source: '/old-blog/:slug', destination: '/blog/:slug', permanent: true },
];
},
// Rewrites
async rewrites() {
return [
{ source: '/api/:path*', destination: 'https://backend.example.com/:path*' },
];
},
// Experimental features
experimental: {
serverActions: { bodySizeLimit: '2mb' },
},
// Output mode
output: 'standalone', // For Docker deployment
// Compression
compress: true,
};
export default nextConfig;# ── Docker Deployment ──
FROM node:20-alpine AS base
# Install dependencies only when needed
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Build stage
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Production stage
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]# ── Server-only (not exposed to client) ──
DATABASE_URL="postgresql://..."
AUTH_SECRET="your-secret-key"
API_INTERNAL_KEY="secret-key"
# ── Public (prefixed with NEXT_PUBLIC_) ──
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_SITE_NAME="My App"
NEXT_PUBLIC_GA_ID="G-XXXXXXX"{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"analyze": "ANALYZE=true next build"
}
}| Strategy | When | How |
|---|---|---|
| Static (SSG) | Build time | Default for pages w/o dynamic() |
| Dynamic (SSR) | Per request | dynamic() or no-store fetch |
| ISR | Periodic | revalidate = N seconds |
| Streaming | Progressive | Suspense boundaries |
| Client | Browser | use client + SWR/Query |
| Feature | Description |
|---|---|
| Image | Automatic WebP/AVIF, lazy loading, blur placeholder |
| Font | next/font: zero layout shift, self-hosted |
| Script | next/script: strategic loading positions |
| Link | Prefetching, client-side navigation |
| Bundle | Automatic code splitting per route |
| Caching | Built-in fetch dedup & caching |
output: 'standalone' in next.config.ts for Docker deployments. This creates a self-contained build with minimal dependencies. Set NODE_ENV=production in your deployment environment for optimal performance.React 18+ Architecture
React Server Components (RSC) render on the server and send HTML to the client with zero JavaScript. They can directly access databases, file systems, and environment variables. Client Components (marked with 'use client') hydrate on the browser and support state, effects, event handlers, and browser APIs. Server Components are the default in the App Router.
| Feature | App Router | Pages Router |
|---|---|---|
| Layouts | Built-in (layout.tsx) | Manual (_app.tsx) |
| Loading States | loading.tsx (Suspense) | Custom implementation |
| Error Handling | error.tsx (Error Boundary) | Custom _error.tsx |
| Server Components | Default | getServerSideProps/getStaticProps |
| Parallel Routes | Yes (@folder) | No |
| Intercepting Routes | Yes ((.) notation) | No |
| Streaming | Built-in | Manual |
ISR combines static generation with periodic updates. Pages are generated at build time (fast), then revalidated in the background at a configured interval. Use revalidate = 60 for time-based ISR or revalidatePath() / revalidateTag() for on-demand revalidation after data changes. This gives you static performance with dynamic freshness.
Server Actions are async functions that run on the server, defined with 'use server'. They simplify mutations by replacing API routes + client fetch with direct form bindings. Use them for form submissions, data mutations, and any operation that modifies server state. They work with React 19's useActionState for pending states and progressive enhancement.
Middleware runs before a request is completed, on the Edge Runtime. It can modify request/response headers, redirect, rewrite, or short-circuit responses. Use it for authentication, rate limiting, A/B testing, i18n routing, and bot protection. The matcher config controls which routes trigger the middleware. Keep it lightweight since it runs on every matched request.
Parallel Routes (@folder) let you render multiple pages in the same layout simultaneously, like a dashboard with independent panels. Each slot loads independently.Intercepting Routes ((.), (..),(...)) let you load a route within the current layout context (e.g., showing a photo modal over the feed without navigating away). Combined, they enable complex UI patterns like modals, split views, and multi-panel layouts.
The '<Image />' component automatically optimizes images: serves WebP/AVIF, lazy loads below the fold, prevents layout shift with aspect ratio, and provides blur placeholder support. It resizes images on-demand through the built-in image optimization API. Configure allowed domains in next.config.ts under images.remotePatterns. For static imports, optimization happens at build time.
| Method | Scope | Use Case |
|---|---|---|
| revalidatePath("/blog") | Specific path | After updating a blog post |
| revalidatePath("/", "layout") | Path + type | Revalidate layout cache |
| revalidateTag("products") | All with tag | After product catalog update |
| revalidateTag("user-data") | All with tag | After user profile change |
Add tags to fetch calls: fetch(url, { next: { tags: ['products'] } })then trigger revalidation for all resources with that tag.