Next.js Framework
Build scalable, production-ready full-stack React applications with Next.js 15+ App Router architecture.
Overview
Next.js is a React framework for building full-stack web applications with built-in routing, rendering strategies (SSR, SSG, ISR), Server Components, Server Actions, and deployment optimization. The App Router (Next.js 13+) represents a fundamental shift toward server-first architecture with React Server Components as the default.
App Router Architecture
File-System Based Routing
| File | Purpose | Example |
|---|---|---|
page.tsx |
Route UI, publicly accessible | app/dashboard/page.tsx → /dashboard |
layout.tsx |
Shared UI wrapper, persists across routes | app/dashboard/layout.tsx |
loading.tsx |
Loading UI with Suspense | app/dashboard/loading.tsx |
error.tsx |
Error boundary UI | app/dashboard/error.tsx |
not-found.tsx |
404 UI | app/not-found.tsx |
route.ts |
API endpoint (GET, POST, etc.) | app/api/users/route.ts |
template.tsx |
Re-renders on navigation (unlike layout) | app/template.tsx |
default.tsx |
Parallel route fallback | app/@modal/default.tsx |
Dynamic Routes
// app/blog/[slug]/page.tsx
interface PageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default async function BlogPost({ params, searchParams }: PageProps) {
const post = await getPost(params.slug);
return <article>{post.content}</article>;
}
// Generate static params for SSG
export async function generateStaticParams() {
const posts = await getPosts();
return posts.map(post => ({ slug: post.slug }));
}
Route Groups and Organization
app/
(marketing)/ # Route group (not in URL)
page.tsx # /
about/page.tsx # /about
(shop)/
products/page.tsx # /products
(auth)/
login/page.tsx # /login
Server vs Client Components
Server Components (Default)
Benefits: Zero client JS, direct backend access, automatic code splitting
// app/users/page.tsx - Server Component by default
import { db } from '@/lib/database';
export default async function UsersPage() {
const users = await db.user.findMany(); // Direct DB access
return (
<div>
{users.map(user => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
Client Components
Use when: Interactivity, hooks, browser APIs needed
// components/Counter.tsx
"use client";
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button => setCount(count + 1)}>{count}</button>;
}
Composition Pattern
// Server Component
export default async function Page() {
const data = await fetchData();
return (
<ClientWrapper>
{/* Server Component passed as children */}
<ServerDataDisplay data={data} />
</ClientWrapper>
);
}
Data Fetching Patterns
Server Component Data Fetching
// Parallel fetching
async function getData() {
const [users, posts] = await Promise.all([
fetch('https://api.example.com/users'),
fetch('https://api.example.com/posts'),
]);
return { users, posts };
}
// Sequential fetching (when dependent)
async function getUser(id: string) {
const user = await fetch(`/api/users/${id}`);
const posts = await fetch(`/api/users/${id}/posts`); // Waits for user
return { user, posts };
}
Fetch Caching Options
// Cached by default (force-cache)
fetch('https://api.example.com/data');
// Opt out of caching
fetch('https://api.example.com/data', { cache: 'no-store' });
// Revalidate every 60 seconds (ISR)
fetch('https://api.example.com/data', { next: { revalidate: 60 } });
// Tag-based revalidation
fetch('https://api.example.com/data', { next: { tags: ['products'] } });
Route Segment Config
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // SSR (no caching)
export const revalidate = 3600; // ISR (revalidate hourly)
export const runtime = 'edge'; // Edge runtime
export default async function Dashboard() {
const data = await fetchData();
return <div>{data}</div>;
}
Server Actions
Server Actions enable calling server code from Client Components without API routes.
Defining Server Actions
// app/actions.ts
"use server";
import { db } from '@/lib/database';
import { revalidatePath } from 'next/cache';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;
await db.post.create({ data: { title, content } });
revalidatePath('/posts');
return { success: true };
}
Using in Forms
// components/PostForm.tsx
"use client";
import { createPost } from '@/app/actions';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return <button disabled={pending}>{pending ? 'Creating...' : 'Create'}</button>;
}
export function PostForm() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<SubmitButton />
</form>
);
}
Programmatic Invocation
"use client";
import { useTransition } from 'react';
import { deletePost } from '@/app/actions';
export function DeleteButton({ postId }: { postId: string }) {
const [isPending, startTransition] = useTransition();
return (
<button
=> startTransition(() => deletePost(postId))}
disabled={isPending}
>
{isPending ? 'Deleting...' : 'Delete'}
</button>
);
}
Streaming and Suspense
Streaming with loading.tsx
// app/dashboard/loading.tsx
export default function Loading() {
return <DashboardSkeleton />;
}
// app/dashboard/page.tsx
export default async function Dashboard() {
const data = await fetchData(); // Streams while loading
return <DashboardContent data={data} />;
}
Granular Suspense Boundaries
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
<Suspense fallback={<Skeleton />}>
<AnotherSlowComponent />
</Suspense>
</div>
);
}
Caching and Revalidation
Cache Layers
- Request Memoization: Automatic deduplication of fetch requests
- Data Cache: Persistent cache across requests and deployments
- Full Route Cache: Pre-rendered HTML and RSC payload
- Router Cache: Client-side cache of visited routes
Revalidation Strategies
// Time-based revalidation
export const revalidate = 3600; // 1 hour
// On-demand revalidation
import { revalidatePath, revalidateTag } from 'next/cache';
await revalidatePath('/posts');
await revalidateTag('products');
Middleware
Run code before request completion for authentication, redirects, rewrites.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
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.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
// Dynamic routes
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await db.user.findUnique({ where: { id: params.id } });
return NextResponse.json(user);
}
Image Optimization
import Image from 'next/image';
<Image
src="/photo.jpg"
alt="Photo"
width={500}
height={300}
priority // Load immediately (above fold)
placeholder="blur" // Show blur while loading
blurDataURL="data:image/..." // Custom blur
/>
// Remote images (configure in next.config.js)
<Image
src="https://i.ytimg.com/vi/sd8ggbUytkk/maxresdefault.jpg"
alt="Photo"
width={500}
height={300}
loader={({ src, width }) => `${src}?w=${width}`}
/>
Metadata and SEO
Static Metadata
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'My App',
description: 'App description',
openGraph: {
title: 'My App',
description: 'App description',
images: ['/og-image.jpg'],
},
};
Dynamic Metadata
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.id);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.coverImage],
},
};
}
Performance Optimization
Code Splitting
import dynamic from 'next/dynamic';
const DynamicComponent = dynamic(() => import('@/components/Heavy'), {
loading: () => <Skeleton />,
ssr: false, // Disable SSR for this component
});
Font Optimization
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
const robotoMono = Roboto_Mono({ subsets: ['latin'] });
export default function Layout({ children }) {
return (
<html className={inter.className}>
<body>{children}</body>
</html>
);
}
Deployment
Vercel (Recommended)
npm install -g vercel
vercel
Docker
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Environment Variables
# .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
Using the Reference Files
When to Read Each Reference
/references/app-router-patterns.md — Read when implementing complex routing scenarios, parallel routes, intercepting routes, or route groups.
/references/data-fetching-caching.md — Read when optimizing data fetching strategies, implementing ISR, or debugging cache behavior.
/references/server-actions-forms.md — Read when building forms with Server Actions, implementing mutations, or handling file uploads.
/references/deployment-optimization.md — Read when preparing for production deployment, optimizing bundle size, or configuring custom infrastructure.
/references/authentication-patterns.md — Read when implementing authentication with NextAuth.js, middleware-based auth, or session management.