Next.js is a React framework for building full-stack web applications.
Handles routing, rendering, data fetching, bundling, and optimization out of the box.
Two routers: App Router (modern, recommended) and Pages Router (legacy, still supported).
Deployed easily on Vercel — also works on any Node.js server or Docker.
Advantages
File-based routing — no router config needed.
Multiple rendering modes: SSR, SSG, ISR, CSR — per page.
Server Components — run React on the server, zero JS sent to client.
Built-in image, font, and script optimization.
API routes — build backend endpoints in the same project.
TypeScript support out of the box.
Excellent DX — fast refresh, error overlay, Turbopack.
Disadvantages
App Router has a learning curve (Server vs Client Components).
Caching behavior is complex and sometimes surprising.
Vendor lock-in risk with Vercel-specific features.
Large bundle size for complex apps.
Opinionated — less flexible than plain React + Vite.
Setup
Create New Project
npx create-next-app@latest my-app# prompts: TypeScript? ESLint? Tailwind? App Router? src/ dir?cd my-appnpm run dev # start dev server (localhost:3000)npm run build # production buildnpm run start # start production servernpm run lint # run ESLint
File Purpose
page.tsx UI for a route — makes route publicly accessible
layout.tsx Shared UI wrapping child routes (persists on navigation)
loading.tsx Loading UI (Suspense boundary) shown while page loads
error.tsx Error UI for the segment (must be Client Component)
not-found.tsx UI for notFound() calls or unmatched routes
route.ts API endpoint (no UI)
template.tsx Like layout but re-mounts on navigation
middleware.ts Runs before requests (at root, not in app/)
// app/blog/loading.tsx — shown while page.tsx is loadingexport default function Loading() { return <div>Loading posts...</div>;}// app/blog/error.tsx — must be Client Component'use client';export default function Error({ error, reset,}: { error: Error; reset: () => void;}) { return ( <div> <h2>Something went wrong: {error.message}</h2> <button onClick={reset}>Try again</button> </div> );}
Server vs Client Components
Comparison
Feature Server Component Client Component
Default in App Router Yes No (opt-in)
Directive (none) 'use client' at top
Runs on Server only Browser (+ server hydration)
Access DB / secrets Yes No
Use useState / useEffect No Yes
Use browser APIs No Yes
JS sent to client No Yes
Can fetch data directly Yes Via useEffect / SWR
Server Component (default)
// No 'use client' — this is a Server Component// Can be async, can fetch data, can access DBasync function getPosts() { const res = await fetch('https://api.example.com/posts'); return res.json();}export default async function BlogPage() { const posts = await getPosts(); // runs on server return ( <ul> {posts.map(post => ( <li key={post.id}>{post.title}</li> ))} </ul> );}
Client Component
'use client'; // must be first lineimport { useState } from 'react';export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(c => c + 1)}> Increment </button> </div> );}
Mixing Server & Client
// Server Component — fetches dataimport LikeButton from './LikeButton'; // Client Componentexport default async function Post({ id }: { id: string }) { const post = await db.posts.findById(id); // server-only return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> {/* Pass serializable data to Client Component */} <LikeButton postId={post.id} initialLikes={post.likes} /> </article> );}
Data Fetching
fetch in Server Components
// Default: cached (like SSG)const data = await fetch('https://api.example.com/data');// No cache (like SSR — fresh every request)const data = await fetch('https://api.example.com/data', { cache: 'no-store',});// Revalidate every N seconds (like ISR)const data = await fetch('https://api.example.com/data', { next: { revalidate: 60 }, // revalidate every 60 seconds});// Tag-based revalidationconst data = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] },});
Rendering Modes
Mode How When to use
SSG fetch() with cache (default) Static content, rarely changes
SSR fetch() with cache: 'no-store' User-specific, real-time data
ISR fetch() with next.revalidate Mostly static, periodic updates
CSR useEffect / SWR / React Query Client-only, interactive data
generateStaticParams — Static dynamic routes
// app/blog/[slug]/page.tsx// Tell Next.js which slugs to pre-render at build timeexport async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return posts.map((post: { slug: string }) => ({ slug: post.slug, }));}export default function BlogPost({ params }: { params: { slug: string } }) { return <h1>{params.slug}</h1>;}
Revalidation
// Revalidate by tag (call from Server Action or API route)import { revalidateTag, revalidatePath } from 'next/cache';revalidateTag('posts'); // invalidate all fetches tagged 'posts'revalidatePath('/blog'); // invalidate specific pathrevalidatePath('/blog/[slug]', 'page'); // invalidate dynamic path
API Routes (Route Handlers)
Basic Route Handler
// app/api/users/route.tsimport { NextRequest, NextResponse } from 'next/server';// GET /api/usersexport async function GET(request: NextRequest) { const users = await db.users.findAll(); return NextResponse.json(users);}// POST /api/usersexport async function POST(request: NextRequest) { const body = await request.json(); const user = await db.users.create(body); return NextResponse.json(user, { status: 201 });}
app/blog/[slug]/page.tsx → /blog/hello-world
app/shop/[...slug]/page.tsx → /shop/a/b/c (catch-all)
app/shop/[[...slug]]/page.tsx → /shop AND /shop/a/b (optional catch-all)
app/(marketing)/about/page.tsx → /about (route group — no URL segment)
app/@modal/(.)photo/page.tsx → parallel route / intercepting route
Navigation
import Link from 'next/link';import { useRouter } from 'next/navigation'; // App Router// Declarative navigation<Link href="/about">About</Link><Link href={`/blog/${slug}`}>Post</Link><Link href="/blog" prefetch={false}>Blog</Link>// Programmatic navigation (Client Component only)const router = useRouter();router.push('/dashboard');router.replace('/login'); // no history entryrouter.back();router.refresh(); // re-fetch server data for current route// Read current path/paramsimport { usePathname, useSearchParams } from 'next/navigation';const pathname = usePathname(); // '/blog/hello'const searchParams = useSearchParams();const page = searchParams.get('page');
Redirect & notFound
import { redirect, notFound } from 'next/navigation';// In Server Components / Server Actionsif (!user) redirect('/login');if (!post) notFound(); // renders not-found.tsx// Permanent redirectimport { permanentRedirect } from 'next/navigation';permanentRedirect('/new-url');
Middleware
middleware.ts
Runs at the Edge before every request — before rendering and API routes.
Place at project root (same level as app/).
// middleware.tsimport { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) { const token = request.cookies.get('token')?.value; const isAuthPage = request.nextUrl.pathname.startsWith('/login'); const isProtected = request.nextUrl.pathname.startsWith('/dashboard'); if (isProtected && !token) { return NextResponse.redirect(new URL('/login', request.url)); } if (isAuthPage && token) { return NextResponse.redirect(new URL('/dashboard', request.url)); } return NextResponse.next();}// Only run middleware on these pathsexport const config = { matcher: ['/dashboard/:path*', '/login'],};
Metadata & SEO
Static Metadata
import type { Metadata } from 'next';export const metadata: Metadata = { title: 'My Blog', description: 'A blog about web development', keywords: ['nextjs', 'react', 'typescript'], openGraph: { title: 'My Blog', description: 'A blog about web development', images: ['/og-image.png'], }, twitter: { card: 'summary_large_image', title: 'My Blog', },};
Dynamic Metadata
// app/blog/[slug]/page.tsximport type { Metadata } from 'next';export async function generateMetadata( { params }: { params: { slug: string } }): Promise<Metadata> { const post = await getPost(params.slug); return { title: post.title, description: post.excerpt, openGraph: { images: [post.coverImage], }, };}
Built-in Components
Image Optimization
import Image from 'next/image';// Local image (auto width/height from import)import profilePic from './profile.jpg';<Image src={profilePic} alt="Profile" />// Remote image (must specify width + height)<Image src="https://example.com/photo.jpg" alt="Photo" width={800} height={600} priority // load eagerly (above the fold) placeholder="blur" // show blur while loading/>// Fill parent container<div style={{ position: 'relative', height: '400px' }}> <Image src="/hero.jpg" alt="Hero" fill style={{ objectFit: 'cover' }} /></div>
Font Optimization
// app/layout.tsximport { Inter, Roboto_Mono } from 'next/font/google';const inter = Inter({ subsets: ['latin'], variable: '--font-inter',});export default function RootLayout({ children }) { return ( <html lang="en" className={inter.variable}> <body className={inter.className}>{children}</body> </html> );}
Script Loading
import Script from 'next/script';// afterInteractive — load after page is interactive (default)<Script src="https://analytics.example.com/script.js" strategy="afterInteractive" />// lazyOnload — load during idle time<Script src="https://widget.example.com/chat.js" strategy="lazyOnload" />// beforeInteractive — load before hydration (use sparingly)<Script src="/critical.js" strategy="beforeInteractive" />
# .env.local — never commitDB_URL=postgresql://localhost/mydbJWT_SECRET=supersecret# Exposed to browser — prefix with NEXT_PUBLIC_NEXT_PUBLIC_API_URL=https://api.example.comNEXT_PUBLIC_GA_ID=G-XXXXXXXXXX