Next.js Best Practices & Patterns
Complete Next.js guide from fundamentals to advanced. Learn App Router, Server Components, data fetching patterns, caching strategies, performance optimization, routing, API routes, middleware, and production-ready patterns used by senior engineers. Master Next.js step-by-step with extensive real-world examples.
๐ฆ 1. Next.js Fundamentals & App Router
Welcome to the foundation of modern Next.js! ๐๏ธ The App Router is a game-changer โ file-based routing that just makes sense. Drop a file, get a route. It's like magic, but better because it's deterministic! โจ
๐ด Impact: CRITICAL โ The App Router is the backbone of every Next.js app โ nail these fundamentals and everything else clicks into place! ๐งฉ
๐ In this section: App Router Architecture โข File-based Routing โข Layouts & Templates โข Loading & Error States โข Route Groups
Key Concepts:
โข App Router: Modern routing system based on React Server Components
โข File-based Routing: Routes are created by adding files to the `app` directory
โข Server Components: Components that render on the server by default
โข Client Components: Interactive components that run in the browser
โข Layouts: Shared UI that wraps multiple pages
โข Loading States: Built-in loading UI while data fetches
// โ Next.js App Router Structureapp/layout.tsx // Root layout (required)page.tsx // Home page (/)loading.tsx // Loading UIerror.tsx // Error UInot-found.tsx // 404 pageglobals.css // Global stylesdashboard/layout.tsx // Dashboard layoutpage.tsx // /dashboardloading.tsx // Dashboard loading statesettings/page.tsx // /dashboard/settingslayout.tsx // Settings layoutblog/page.tsx // /blog (list)[slug]/page.tsx // /blog/[slug] (dynamic route)(marketing)/ // Route group (doesn't affect URL)about/page.tsx // /aboutcontact/page.tsx // /contactapi/users/route.ts // API route: /api/users[id]/route.ts // API route: /api/users/[id]// โ Root Layout (app/layout.tsx)import type { Metadata } from 'next';import { Inter } from 'next/font/google';import './globals.css';const inter = Inter({subsets: ['latin'],display: 'swap',variable: '--font-inter',});export const metadata: Metadata = {title: {default: 'My App',template: '%s | My App',},description: 'Production-ready Next.js application',metadataBase: new URL('https://example.com'),openGraph: {type: 'website',locale: 'en_US',url: 'https://example.com',siteName: 'My App',},twitter: {card: 'summary_large_image',creator: '@username',},};export default function RootLayout({children,}: {children: React.ReactNode;}) {return (<html lang="en" className={inter.variable}><body className={inter.className}>{children}</body></html>);}// โ Page Component (app/page.tsx)// By default, this is a Server Componentexport default function HomePage() {// This runs on the serverconst timestamp = new Date().toISOString();return (<main><h1>Welcome to Next.js</h1><p>Server rendered at: {timestamp}</p></main>);}// โ Layout Component (app/dashboard/layout.tsx)export default function DashboardLayout({children,}: {children: React.ReactNode;}) {return (<div className="dashboard-container"><aside><nav><a href="/dashboard">Dashboard</a><a href="/dashboard/settings">Settings</a></nav></aside><main>{children}</main></div>);}
โก 2. Server Components vs Client Components
This is THE most important concept in modern Next.js! ๐ง Server Components render on the server (zero JS shipped to the client), while Client Components handle interactivity. Master this split and your apps will be blazing fast! ๐ฅ
๐ด Impact: CRITICAL โ Getting the Server/Client boundary right is the #1 factor in Next.js app performance โ mess this up and everything suffers! โ ๏ธ
๐ In this section: Server vs Client Components โข When to Use Each โข Data Fetching Patterns โข Composition Strategies โข Bundle Size Optimization
// โ WRONG: Using Client Component for everything"use client";import { useState, useEffect } from 'react';export default function ProductPage() {const [products, setProducts] = useState([]);const [loading, setLoading] = useState(true);useEffect(() => {// Fetching on client side - slower, exposes APIfetch('/api/products').then(res => res.json()).then(data => {setProducts(data);setLoading(false);});}, []);if (loading) return <div>Loading...</div>;return (<div>{products.map(product => (<div key={product.id}>{product.name}</div>))}</div>);}// Problems:// - All JavaScript shipped to client// - Client-side data fetching (slower)// - Exposes API endpoints// - Worse SEO// - Larger bundle size
// โ CORRECT: Server Component for data fetching// app/products/page.tsx (Server Component - default)import { Suspense } from 'react';import ProductList from './components/ProductList';import LoadingSkeleton from './components/LoadingSkeleton';// Server Component - runs on serverasync function getProducts() {// Direct database access or API call on serverconst res = await fetch('https://api.example.com/products', {cache: 'force-cache', // Cache the resultnext: { revalidate: 3600 }, // Revalidate every hour});if (!res.ok) throw new Error('Failed to fetch products');return res.json();}export default async function ProductsPage() {// This runs on the server - no JavaScript sent to clientconst products = await getProducts();return (<main><h1>Products</h1><Suspense fallback={<LoadingSkeleton />}><ProductList products={products} /></Suspense></main>);}// โ Client Component only when needed// app/products/components/ProductList.tsx"use client";import { useState } from 'react';export default function ProductList({ products }: { products: Product[] }) {const [filter, setFilter] = useState('');// Interactive UI - needs client-side JavaScriptconst filtered = products.filter(p =>p.name.toLowerCase().includes(filter.toLowerCase()));return (<div><inputvalue={filter}onChange={(e) => setFilter(e.target.value)}placeholder="Filter products..."/>{filtered.map(product => (<ProductCard key={product.id} product={product} />))}</div>);}// โ Server Component with Metadataexport const metadata = {title: 'Products',description: 'Browse our amazing products',};// โ Pattern: Server Component fetches, Client Component renders interactively// Server Component (page.tsx)async function DashboardPage() {const data = await fetchDashboardData(); // Server-sidereturn (<DashboardClient initialData={data} />);}// Client Component (interactive features)"use client";function DashboardClient({ initialData }) {const [filters, setFilters] = useState({});// Use initialData, add client-side interactivityreturn <DashboardUI data={initialData} filters={filters} />;}
๐ 3. Data Fetching Patterns & Best Practices
Data fetching in Next.js is an art form! ๐จ From async Server Components to parallel requests and Suspense streaming โ there's a pattern for every situation. Say goodbye to loading spinners and hello to instant data! ๐๏ธ๐จ
๐ด Impact: CRITICAL โ How you fetch data determines your app's speed and user experience โ these patterns are the difference between 100ms and 3s load times! โฑ๏ธ
๐ In this section: Async Server Components โข Parallel & Sequential Fetching โข Suspense Streaming โข Loading States โข Error Handling โข Route Handlers
// โ Pattern 1: Async Server Component// app/posts/page.tsxasync function getPosts() {const res = await fetch('https://api.example.com/posts', {next: { revalidate: 3600 }, // Revalidate every hour});if (!res.ok) {throw new Error('Failed to fetch posts');}return res.json();}export default async function PostsPage() {const posts = await getPosts(); // Awaits on serverreturn (<div>{posts.map(post => (<article key={post.id}><h2>{post.title}</h2><p>{post.content}</p></article>))}</div>);}// โ Pattern 2: Parallel Data Fetchingasync function DashboardPage() {// Fetch in parallel - both requests start simultaneouslyconst [user, posts] = await Promise.all([fetch('https://api.example.com/user').then(r => r.json()),fetch('https://api.example.com/posts').then(r => r.json()),]);return (<div><UserProfile user={user} /><PostsList posts={posts} /></div>);}// โ Pattern 3: Sequential Data Fetching (when dependencies exist)async function PostDetailPage({ params }: { params: { id: string } }) {// Fetch post firstconst post = await fetch(`https://api.example.com/posts/\${params.id}`).then(r => r.json());// Then fetch author (depends on post data)const author = await fetch(`https://api.example.com/authors/\${post.authorId}`).then(r => r.json());return (<article><h1>{post.title}</h1><p>By {author.name}</p><div>{post.content}</div></article>);}// โ Pattern 4: Streaming with Suspense// app/dashboard/page.tsximport { Suspense } from 'react';async function Revenue() {const revenue = await fetch('https://api.example.com/revenue', {cache: 'no-store', // Don't cache}).then(r => r.json());return <div>Revenue: \${revenue.total}</div>;}async function Users() {const users = await fetch('https://api.example.com/users', {cache: 'no-store',}).then(r => r.json());return <div>Users: {users.count}</div>;}export default function DashboardPage() {return (<div>{/* Stream each section independently */}<Suspense fallback={<RevenueSkeleton />}><Revenue /></Suspense><Suspense fallback={<UsersSkeleton />}><Users /></Suspense></div>);}// โ Pattern 5: Loading States// app/posts/loading.tsxexport default function Loading() {return (<div className="animate-pulse"><div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div><div className="h-4 bg-gray-200 rounded w-full mb-2"></div><div className="h-4 bg-gray-200 rounded w-5/6"></div></div>);}// โ Pattern 6: Error Handling// app/posts/error.tsx"use client";export default function Error({error,reset,}: {error: Error & { digest?: string };reset: () => void;}) {return (<div><h2>Something went wrong!</h2><button onClick={() => reset()}>Try again</button></div>);}// โ Pattern 7: Route Handlers (API Routes)// app/api/posts/route.tsimport { NextRequest, NextResponse } from 'next/server';export async function GET(request: NextRequest) {const searchParams = request.nextUrl.searchParams;const page = searchParams.get('page') || '1';const posts = await getPostsFromDB(parseInt(page));return NextResponse.json({ posts });}export async function POST(request: NextRequest) {const body = await request.json();// Validate inputif (!body.title || !body.content) {return NextResponse.json({ error: 'Title and content required' },{ status: 400 });}const post = await createPost(body);return NextResponse.json({ post }, { status: 201 });}// โ Pattern 8: Dynamic Route Handlers// app/api/posts/[id]/route.tsexport async function GET(request: NextRequest,{ params }: { params: { id: string } }) {const post = await getPostById(params.id);if (!post) {return NextResponse.json({ error: 'Post not found' },{ status: 404 });}return NextResponse.json({ post });}export async function DELETE(request: NextRequest,{ params }: { params: { id: string } }) {await deletePost(params.id);return NextResponse.json({ success: true });}
๐๏ธ 4. Caching Strategies & Data Revalidation
Caching is your secret weapon for blazing-fast apps! ๐ Next.js has FOUR levels of caching built in, and mastering them is like having a turbo button for your entire application. Fresh data when you need it, cached speed when you don't! ๐จ
๐ Impact: HIGH โ Proper caching can reduce server costs by 90% and make your app feel instant โ this is where senior engineers shine! ๐
๐ In this section: Request Memoization โข Data Cache โข Full Route Cache โข Time-based Revalidation โข On-demand Revalidation โข ISR
Caching Levels in Next.js:
โข Request Memoization: Automatically deduplicates fetch requests
โข Data Cache: Persistent cache for fetch requests
โข Full Route Cache: Caches entire rendered page
โข Router Cache: Client-side cache for navigation
โข Revalidation: Time-based or on-demand cache invalidation
// โ Caching Strategy 1: Time-based Revalidationasync function getProducts() {const res = await fetch('https://api.example.com/products', {// Cache for 1 hour, then revalidate in backgroundnext: { revalidate: 3600 },});return res.json();}// โ Caching Strategy 2: On-demand Revalidation// app/api/revalidate/route.tsimport { revalidatePath, revalidateTag } from 'next/cache';import { NextRequest, NextResponse } from 'next/server';export async function POST(request: NextRequest) {const { path, tag } = await request.json();if (path) {revalidatePath(path);}if (tag) {revalidateTag(tag);}return NextResponse.json({ revalidated: true });}// Use tags for cache invalidationasync function getProduct(id: string) {const res = await fetch(`https://api.example.com/products/\${id}`, {next: {tags: ['products', `product-\${id}`],revalidate: 3600,},});return res.json();}// โ Caching Strategy 3: Opt-out of Cachingasync function getRealTimeData() {// Always fetch fresh data (no cache)const res = await fetch('https://api.example.com/realtime', {cache: 'no-store',});return res.json();}// โ Caching Strategy 4: Force Cacheasync function getStaticData() {// Cache indefinitely (until manually revalidated)const res = await fetch('https://api.example.com/static-content', {cache: 'force-cache',});return res.json();}// โ Caching Strategy 5: Dynamic Functions (Opt-out of static generation)// app/dynamic/page.tsxexport const dynamic = 'force-dynamic'; // Always render dynamicallyexport const revalidate = 0; // Don't cacheexport default async function DynamicPage() {const data = await fetch('https://api.example.com/data', {cache: 'no-store',}).then(r => r.json());return <div>{data.value}</div>;}// โ Caching Strategy 6: Segment Configexport const fetchCache = 'force-no-store'; // Don't cache any fetch in this routeexport const runtime = 'nodejs'; // Use Node.js runtimeexport const preferredRegion = 'us-east-1'; // Deploy to specific region// โ Caching Strategy 7: Route Segment Config// app/products/page.tsxexport const dynamic = 'force-static'; // Force static generationexport const revalidate = false; // Never revalidate (fully static)// โ Caching Strategy 8: Advanced Revalidation Pattern// app/api/products/route.tsexport async function POST(request: NextRequest) {const product = await createProduct(await request.json());// Revalidate product list and specific productrevalidatePath('/products');revalidatePath(`/products/\${product.id}`);revalidateTag('products');revalidateTag(`product-\${product.id}`);return NextResponse.json({ product });}// โ Caching Strategy 9: Conditional Cachingasync function getUserData(userId: string, forceRefresh?: boolean) {const res = await fetch(`https://api.example.com/users/\${userId}`, {cache: forceRefresh ? 'no-store' : 'force-cache',next: forceRefresh ? undefined : { revalidate: 3600 },});return res.json();}// โ Caching Strategy 10: Incremental Static Regeneration (ISR)// app/blog/[slug]/page.tsxexport async function generateStaticParams() {const posts = await fetch('https://api.example.com/posts').then(r => r.json());return posts.map((post: Post) => ({slug: post.slug,}));}export default async function BlogPost({ params }: { params: { slug: string } }) {const post = await fetch(`https://api.example.com/posts/\${params.slug}`, {next: { revalidate: 86400 }, // Revalidate once per day}).then(r => r.json());return <article>{/* Post content */}</article>;}
๐บ๏ธ 5. Advanced Routing Patterns
Next.js routing goes WAY beyond basic pages! ๐คฏ Dynamic segments, catch-all routes, parallel routes, intercepting routes โ it's like having a GPS that can navigate any road. Build complex UIs with elegant URL structures! ๐ค๏ธ
๐ Impact: HIGH โ Advanced routing unlocks modals, dashboards, and multi-view layouts that feel native โ your users will love it! โค๏ธ
๐ In this section: Dynamic Routes โข Catch-all Segments โข Route Groups โข Parallel Routes โข Intercepting Routes โข Programmatic Navigation
// โ Pattern 1: Dynamic Routes// app/products/[id]/page.tsxexport default async function ProductPage({params}: {params: { id: string }}) {const product = await getProduct(params.id);return (<div><h1>{product.name}</h1><p>{product.description}</p></div>);}// โ Pattern 2: Catch-all Segments// app/shop/[...slug]/page.tsxexport default function ShopPage({params}: {params: { slug: string[] }}) {// Handles /shop, /shop/category, /shop/category/product, etc.const path = params.slug?.join('/') || '';return <div>Shop: {path}</div>;}// โ Pattern 3: Optional Catch-all// app/docs/[[...slug]]/page.tsxexport default function DocsPage({params}: {params: { slug?: string[] }}) {// Handles /docs and /docs/any/pathconst slug = params.slug || [];return <div>Docs: {slug.join('/')}</div>;}// โ Pattern 4: Route Groups (Organize without affecting URL)// app/(marketing)/about/page.tsx โ /about// app/(marketing)/contact/page.tsx โ /contact// app/(dashboard)/settings/page.tsx โ /settings// app/(dashboard)/profile/page.tsx โ /profile// โ Pattern 5: Parallel Routes// app/dashboard/@analytics/page.tsxexport default function Analytics() {return <div>Analytics Content</div>;}// app/dashboard/@team/page.tsxexport default function Team() {return <div>Team Content</div>;}// app/dashboard/layout.tsxexport default function DashboardLayout({children,analytics,team,}: {children: React.ReactNode;analytics: React.ReactNode;team: React.ReactNode;}) {return (<div>{children}<div className="grid grid-cols-2">{analytics}{team}</div></div>);}// โ Pattern 6: Intercepting Routes (Modals)// app/@modal/(.)photo/[id]/page.tsx// Shows modal when navigating to /photo/[id] from within app// app/photo/[id]/page.tsx// Shows full page when accessed directly// โ Pattern 7: Conditional Routes// app/(auth)/login/page.tsx - Requires authentication layout// app/(public)/about/page.tsx - Public layout// โ Pattern 8: Multiple Dynamic Segments// app/posts/[category]/[slug]/page.tsxexport default async function PostPage({params,}: {params: { category: string; slug: string };}) {const post = await getPost(params.category, params.slug);return <article>{/* Post */}</article>;}// โ Pattern 9: Route Handlers with Dynamic Segments// app/api/users/[id]/route.tsexport async function GET(request: NextRequest,{ params }: { params: { id: string } }) {const user = await getUserById(params.id);return NextResponse.json({ user });}// โ Pattern 10: SearchParams// app/search/page.tsxexport default function SearchPage({searchParams,}: {searchParams: { q?: string; page?: string };}) {const query = searchParams.q || '';const page = parseInt(searchParams.page || '1');return (<div><h1>Search: {query}</h1><p>Page: {page}</p></div>);}// โ Pattern 11: Redirects and Rewrites// next.config.jsmodule.exports = {async redirects() {return [{source: '/old-path',destination: '/new-path',permanent: true,},];},async rewrites() {return [{source: '/api/proxy/:path*',destination: 'https://external-api.com/:path*',},];},};// โ Pattern 12: Programmatic Navigation// Client Component"use client";import { useRouter } from 'next/navigation';export default function NavigationButton() {const router = useRouter();const handleClick = () => {router.push('/dashboard');// orrouter.replace('/login'); // Replace current history entry// orrouter.refresh(); // Refresh current route// orrouter.back(); // Go back};return <button onClick={handleClick}>Navigate</button>;}
๐ 6. Middleware & Edge Runtime
Middleware is your app's bouncer at the door! ๐ช It intercepts every request before it reaches your pages โ perfect for auth checks, A/B testing, geo-routing, and more. Running at the edge means millisecond response times worldwide! ๐โก
๐ Impact: HIGH โ Middleware is your first line of defense and optimization โ auth, redirects, headers, all in one place at the edge! ๐ฐ
๐ In this section: Authentication Middleware โข A/B Testing โข Geolocation Routing โข Rate Limiting โข Security Headers โข Bot Detection
// โ Middleware Pattern 1: Basic Setup// middleware.ts (root of project)import { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) {// Middleware runs before request is completedconst response = NextResponse.next();// Add custom headerresponse.headers.set('x-custom-header', 'value');return response;}// Configure which routes middleware runs onexport const config = {matcher: ['/dashboard/:path*','/api/:path*',],};// โ Middleware Pattern 2: Authenticationimport { NextResponse } from 'next/server';import type { NextRequest } from 'next/server';export function middleware(request: NextRequest) {const token = request.cookies.get('auth-token')?.value;// Protected routesif (request.nextUrl.pathname.startsWith('/dashboard')) {if (!token) {return NextResponse.redirect(new URL('/login', request.url));}}// Redirect authenticated users away from loginif (request.nextUrl.pathname === '/login' && token) {return NextResponse.redirect(new URL('/dashboard', request.url));}return NextResponse.next();}export const config = {matcher: ['/dashboard/:path*', '/login'],};// โ Middleware Pattern 3: A/B Testingexport function middleware(request: NextRequest) {const response = NextResponse.next();// Random A/B test variantconst variant = Math.random() > 0.5 ? 'a' : 'b';// Set cookie for consistent experienceresponse.cookies.set('ab-variant', variant, {maxAge: 60 * 60 * 24 * 30, // 30 days});// Add header for analyticsresponse.headers.set('x-ab-variant', variant);return response;}// โ Middleware Pattern 4: Geolocation & Localizationexport function middleware(request: NextRequest) {const country = request.geo?.country || 'US';const locale = request.headers.get('accept-language')?.split(',')[0] || 'en';// Redirect based on country/localeif (country === 'ES' && !request.nextUrl.pathname.startsWith('/es')) {return NextResponse.redirect(new URL('/es' + request.nextUrl.pathname, request.url));}const response = NextResponse.next();response.headers.set('x-country', country);response.headers.set('x-locale', locale);return response;}// โ Middleware Pattern 5: Rate Limitingconst rateLimitMap = new Map();export function middleware(request: NextRequest) {const ip = request.ip || 'unknown';const key = `rate-limit:\${ip}`;const requests = rateLimitMap.get(key) || [];const now = Date.now();const recentRequests = requests.filter((time: number) => now - time < 60000); // Last minuteif (recentRequests.length >= 10) {return new NextResponse('Too Many Requests', { status: 429 });}recentRequests.push(now);rateLimitMap.set(key, recentRequests);return NextResponse.next();}export const config = {matcher: '/api/:path*',};// โ Middleware Pattern 6: Request Loggingexport function middleware(request: NextRequest) {const start = Date.now();const response = NextResponse.next();// Log after responseresponse.headers.set('x-response-time', `\${Date.now() - start}ms`);// Log to analyticsconsole.log({method: request.method,path: request.nextUrl.pathname,ip: request.ip,userAgent: request.headers.get('user-agent'),});return response;}// โ Middleware Pattern 7: Security Headersexport function middleware(request: NextRequest) {const response = NextResponse.next();// Security headersresponse.headers.set('X-DNS-Prefetch-Control', 'on');response.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');response.headers.set('X-Frame-Options', 'SAMEORIGIN');response.headers.set('X-Content-Type-Options', 'nosniff');response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');return response;}// โ Middleware Pattern 8: Feature Flagsexport function middleware(request: NextRequest) {const featureFlag = request.cookies.get('feature-flag')?.value || 'default';const response = NextResponse.next();// Inject feature flag into headersresponse.headers.set('x-feature-flag', featureFlag);// Conditional redirects based on feature flagif (featureFlag === 'beta' && !request.nextUrl.pathname.startsWith('/beta')) {return NextResponse.rewrite(new URL('/beta' + request.nextUrl.pathname, request.url));}return response;}// โ Middleware Pattern 9: Bot Detectionexport function middleware(request: NextRequest) {const userAgent = request.headers.get('user-agent') || '';const isBot = /bot|crawler|spider|crawling/i.test(userAgent);const response = NextResponse.next();if (isBot) {response.headers.set('x-is-bot', 'true');// Serve cached or simplified version for bots}return response;}// โ Middleware Pattern 10: Multi-Region Routingexport function middleware(request: NextRequest) {const region = request.geo?.region || 'us-east';// Route to regional APIif (request.nextUrl.pathname.startsWith('/api/')) {const apiUrl = new URL(request.nextUrl.pathname, `https://api-\${region}.example.com`);return NextResponse.rewrite(apiUrl);}return NextResponse.next();}export const config = {matcher: '/api/:path*',};// โ Edge Runtime Configurationexport const runtime = 'edge'; // Use Edge Runtime for faster cold starts// โ Advanced Matcher Patternsexport const config = {matcher: [// Match all paths except static files and API routes'/((?!api|_next/static|_next/image|favicon.ico).*)',// Or specific patterns'/dashboard/:path*','/admin/:path*',// Exclude patterns'/((?!login|register|_next/static).*)',],};
๐๏ธ 7. Performance Optimization Strategies
Speed is a feature, and Next.js gives you the tools to go FAST! ๐จ From optimized images that load in a blink to fonts that never block rendering โ every millisecond counts. Make your Lighthouse score turn green! ๐๐
๐ด Impact: CRITICAL โ Performance directly affects SEO, conversions, and user satisfaction โ a 1s delay = 7% fewer conversions! ๐
๐ In this section: Image Optimization โข Font Optimization โข Code Splitting โข Dynamic Imports โข Bundle Analysis โข Core Web Vitals
// โ WRONG: Unoptimized images and fontsexport default function HomePage() {return (<div>{/* Unoptimized image - large file, no lazy loading */}<img src="/large-image.jpg" alt="Large image" />{/* Multiple unoptimized images */}<img src="/image1.jpg" /><img src="/image2.jpg" /><img src="/image3.jpg" />{/* No font optimization */}<link href="https://fonts.googleapis.com/css2?family=Inter" rel="stylesheet" /></div>);}// Problems:// - Large bundle sizes// - Slow page loads// - Poor Core Web Vitals// - No image optimization// - Blocking font loading
// โ CORRECT: Optimized images and fontsimport Image from 'next/image';import { Inter } from 'next/font/google';// Optimize fonts at build timeconst inter = Inter({subsets: ['latin'],display: 'swap', // Don't block renderingvariable: '--font-inter',preload: true,});export default function HomePage() {return (<div className={inter.variable}>{/* Optimized Next.js Image component */}<Imagesrc="/large-image.jpg"alt="Large image"width={1200}height={800}priority // Load immediately (above fold)placeholder="blur"blurDataURL="data:image/..." // Low quality placeholder/>{/* Lazy load images below fold */}<Imagesrc="/image1.jpg"alt="Image 1"width={600}height={400}loading="lazy" // Default for below fold/>{/* Responsive images */}<Imagesrc="/hero.jpg"alt="Hero"width={1920}height={1080}sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"className="responsive-image"/></div>);}// โ Image Optimization Configuration// next.config.jsmodule.exports = {images: {formats: ['image/avif', 'image/webp'],deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],remotePatterns: [{protocol: 'https',hostname: 'example.com',pathname: '/images/**',},],},};// โ Font Optimization Best Practices// app/layout.tsximport { Inter, Roboto_Mono } from 'next/font/google';import localFont from 'next/font/local';const inter = Inter({subsets: ['latin'],display: 'swap',variable: '--font-inter',});const robotoMono = Roboto_Mono({subsets: ['latin'],display: 'swap',variable: '--font-mono',});const customFont = localFont({src: './fonts/custom-font.woff2',display: 'swap',variable: '--font-custom',});export default function RootLayout({ children }) {return (<html className={`\${inter.variable} \${robotoMono.variable} \${customFont.variable}`}><body>{children}</body></html>);}// โ Dynamic Imports for Code Splittingimport dynamic from 'next/dynamic';// Lazy load heavy componentconst HeavyChart = dynamic(() => import('./components/HeavyChart'), {loading: () => <ChartSkeleton />,ssr: false, // Disable SSR if component uses browser APIs});export default function DashboardPage() {return (<div><HeavyChart /></div>);}// โ Bundle Analysis// Install: npm install @next/bundle-analyzer// next.config.jsconst withBundleAnalyzer = require('@next/bundle-analyzer')({enabled: process.env.ANALYZE === 'true',});module.exports = withBundleAnalyzer({// Your config});// Run: ANALYZE=true npm run build// โ Performance Monitoring// app/layout.tsxexport function reportWebVitals(metric: any) {// Send to analyticsif (typeof window !== 'undefined') {// Example: send to analytics servicefetch('/api/analytics', {method: 'POST',body: JSON.stringify(metric),});}}
๐ 8. Metadata & SEO Best Practices
Want Google to love your app? ๐ Next.js makes SEO a breeze with built-in metadata APIs, dynamic OG images, and automatic sitemap generation. From rich snippets to Twitter Cards โ make every search result click-worthy! ๐ฏ๐
๐ Impact: HIGH โ Great SEO means free organic traffic โ these patterns are the difference between page 1 and page 10 on Google! ๐
๐ In this section: Static & Dynamic Metadata โข Open Graph โข Twitter Cards โข Structured Data โข Sitemaps โข Dynamic OG Images
// โ Pattern 1: Static Metadata// app/about/page.tsximport type { Metadata } from 'next';export const metadata: Metadata = {title: 'About Us',description: 'Learn more about our company and mission',keywords: ['about', 'company', 'mission'],};export default function AboutPage() {return <div>About content</div>;}// โ Pattern 2: Dynamic Metadata// app/blog/[slug]/page.tsxexport async function generateMetadata({params}: {params: { slug: string }}): Promise<Metadata> {const post = await getPost(params.slug);return {title: post.title,description: post.excerpt,openGraph: {title: post.title,description: post.excerpt,images: [post.image],type: 'article',publishedTime: post.publishedAt,authors: [post.author],},twitter: {card: 'summary_large_image',title: post.title,description: post.excerpt,images: [post.image],},};}// โ Pattern 3: Comprehensive Metadataexport const metadata: Metadata = {title: {default: 'My App',template: '%s | My App',},description: 'Production-ready Next.js application',keywords: ['nextjs', 'react', 'web development'],authors: [{ name: 'John Doe', url: 'https://example.com' }],creator: 'John Doe',publisher: 'My Company',metadataBase: new URL('https://example.com'),alternates: {canonical: '/',languages: {'en-US': '/en-US','es-ES': '/es-ES',},},openGraph: {type: 'website',locale: 'en_US',url: 'https://example.com',siteName: 'My App',title: 'My App',description: 'Production-ready Next.js application',images: [{url: '/og-image.jpg',width: 1200,height: 630,alt: 'My App',},],},twitter: {card: 'summary_large_image',title: 'My App',description: 'Production-ready Next.js application',images: ['/twitter-image.jpg'],creator: '@username',},robots: {index: true,follow: true,googleBot: {index: true,follow: true,'max-video-preview': -1,'max-image-preview': 'large','max-snippet': -1,},},verification: {google: 'verification-token',yandex: 'verification-token',bing: 'verification-token',},};// โ Pattern 4: Structured Data (JSON-LD)// app/components/StructuredData.tsxexport function ProductStructuredData({ product }: { product: Product }) {const structuredData = {'@context': 'https://schema.org','@type': 'Product',name: product.name,description: product.description,image: product.image,offers: {'@type': 'Offer',price: product.price,priceCurrency: 'USD',availability: 'https://schema.org/InStock',},};return (<scripttype="application/ld+json"dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}/>);}// โ Pattern 5: Sitemap Generation// app/sitemap.tsimport { MetadataRoute } from 'next';export default function sitemap(): MetadataRoute.Sitemap {const baseUrl = 'https://example.com';// Static routesconst staticRoutes: MetadataRoute.Sitemap = [{url: baseUrl,lastModified: new Date(),changeFrequency: 'yearly',priority: 1,},{url: `\${baseUrl}/about`,lastModified: new Date(),changeFrequency: 'monthly',priority: 0.8,},];// Dynamic routesconst posts = await getPosts();const postRoutes: MetadataRoute.Sitemap = posts.map((post) => ({url: `\${baseUrl}/blog/\${post.slug}`,lastModified: post.updatedAt,changeFrequency: 'weekly',priority: 0.6,}));return [...staticRoutes, ...postRoutes];}// โ Pattern 6: Robots.txt// app/robots.tsimport { MetadataRoute } from 'next';export default function robots(): MetadataRoute.Robots {return {rules: [{userAgent: '*',allow: '/',disallow: ['/api/', '/admin/'],},{userAgent: 'Googlebot',allow: '/',disallow: ['/admin/'],},],sitemap: 'https://example.com/sitemap.xml',};}// โ Pattern 7: Dynamic Open Graph Images// app/og-image/route.tsximport { ImageResponse } from 'next/og';export const runtime = 'edge';export async function GET(request: Request) {const { searchParams } = new URL(request.url);const title = searchParams.get('title') || 'Default Title';return new ImageResponse((<divstyle={{fontSize: 128,background: 'white',width: '100%',height: '100%',display: 'flex',alignItems: 'center',justifyContent: 'center',}}>{title}</div>),{width: 1200,height: 630,});}// Use: <meta property="og:image" content="/og-image?title=My Post" />// โ Pattern 8: Canonical URLsexport const metadata: Metadata = {alternates: {canonical: 'https://example.com/post/slug',},};// โ Pattern 9: Language Alternatesexport const metadata: Metadata = {alternates: {languages: {'en-US': 'https://example.com/en/post','es-ES': 'https://example.com/es/post','fr-FR': 'https://example.com/fr/post',},},};
๐ 9. Forms & Server Actions
Server Actions are a revolution for form handling! ๐ No more API route boilerplate โ just write a function, mark it 'use server', and boom โ your form talks directly to the server. Add Zod validation and optimistic updates for chef's kiss perfection! ๐จโ๐ณ๐
๐ด Impact: CRITICAL โ Server Actions simplify your entire data mutation layer โ less code, better DX, progressive enhancement for free! ๐
๐ In this section: Server Actions โข Form Validation with Zod โข useFormState Hook โข Optimistic Updates โข File Uploads โข Error Handling
// โ Pattern 1: Server Actions// app/actions.ts'use server';import { revalidatePath } from 'next/cache';import { redirect } from 'next/navigation';export async function createPost(formData: FormData) {const title = formData.get('title') as string;const content = formData.get('content') as string;// Validate inputif (!title || !content) {return { error: 'Title and content are required' };}// Save to databaseconst post = await db.posts.create({title,content,});// Revalidate and redirectrevalidatePath('/posts');redirect(`/posts/\${post.id}`);}// โ Pattern 2: Form with Server Action// app/posts/create/page.tsximport { createPost } from '../actions';export default function CreatePostPage() {return (<form action={createPost}><input name="title" placeholder="Title" required /><textarea name="content" placeholder="Content" required /><button type="submit">Create Post</button></form>);}// โ Pattern 3: Server Action with Validation'use server';import { z } from 'zod';const postSchema = z.object({title: z.string().min(1).max(100),content: z.string().min(10),});export async function createPostWithValidation(prevState: any,formData: FormData) {const rawData = {title: formData.get('title'),content: formData.get('content'),};// Validate with Zodconst result = postSchema.safeParse(rawData);if (!result.success) {return {error: 'Validation failed',issues: result.error.issues,};}// Create postconst post = await db.posts.create(result.data);return { success: true, postId: post.id };}// โ Pattern 4: useFormState Hook'use client';import { useFormState, useFormStatus } from 'react-dom';import { createPostWithValidation } from './actions';function SubmitButton() {const { pending } = useFormStatus();return (<button type="submit" disabled={pending}>{pending ? 'Creating...' : 'Create Post'}</button>);}export default function CreatePostForm() {const [state, formAction] = useFormState(createPostWithValidation, null);return (<form action={formAction}><input name="title" required /><textarea name="content" required />{state?.error && <div className="error">{state.error}</div>}{state?.issues && (<ul>{state.issues.map((issue: any) => (<li key={issue.path}>{issue.message}</li>))}</ul>)}<SubmitButton /></form>);}// โ Pattern 5: Optimistic Updates'use client';import { useOptimistic } from 'react';import { addComment } from './actions';function CommentList({ comments }: { comments: Comment[] }) {const [optimisticComments, addOptimisticComment] = useOptimistic(comments,(state, newComment: Comment) => [...state, newComment]);async function handleSubmit(formData: FormData) {const comment = {id: Date.now().toString(),text: formData.get('text') as string,pending: true,};// Optimistically add commentaddOptimisticComment(comment);// Submit to serverawait addComment(formData);}return (<div><form action={handleSubmit}><input name="text" required /><button type="submit">Add Comment</button></form>{optimisticComments.map((comment) => (<div key={comment.id}>{comment.text}{comment.pending && <span>(Pending...)</span>}</div>))}</div>);}// โ Pattern 6: File Uploads'use server';export async function uploadFile(formData: FormData) {const file = formData.get('file') as File;if (!file) {return { error: 'No file provided' };}// Validate file type and sizeif (file.size > 5 * 1024 * 1024) { // 5MBreturn { error: 'File too large' };}const bytes = await file.arrayBuffer();const buffer = Buffer.from(bytes);// Save file (e.g., to S3, local storage, etc.)const path = await saveFile(buffer, file.name);return { success: true, path };}// โ Pattern 7: Progress Enhancement with Loading States'use client';import { useTransition } from 'react';import { updateProfile } from './actions';export default function ProfileForm({ user }: { user: User }) {const [isPending, startTransition] = useTransition();async function handleSubmit(formData: FormData) {startTransition(async () => {await updateProfile(formData);});}return (<form action={handleSubmit}><input name="name" defaultValue={user.name} /><button type="submit" disabled={isPending}>{isPending ? 'Saving...' : 'Save'}</button></form>);}// โ Pattern 8: Server Action Error Handling'use server';export async function deletePost(id: string) {try {await db.posts.delete(id);revalidatePath('/posts');return { success: true };} catch (error) {console.error('Error deleting post:', error);return {error: 'Failed to delete post',message: error instanceof Error ? error.message : 'Unknown error',};}}// โ Pattern 9: Authentication in Server Actions'use server';import { auth } from '@/lib/auth';import { redirect } from 'next/navigation';export async function createPost(formData: FormData) {const session = await auth();if (!session) {redirect('/login');}// Create post as authenticated userconst post = await db.posts.create({title: formData.get('title'),content: formData.get('content'),authorId: session.user.id,});return { success: true, postId: post.id };}
๐ท 10. TypeScript Patterns for Next.js
TypeScript + Next.js = a match made in heaven! ๐ Type your routes, params, metadata, server actions, and API responses. Your IDE becomes a superpower with autocomplete everywhere and errors caught before they ship! ๐ฏ๐ก๏ธ
๐ต Impact: MEDIUM โ Proper TypeScript patterns prevent runtime errors and make refactoring a joy instead of a nightmare! ๐
๐ In this section: Typed Route Parameters โข Typed Search Params โข Typed Server Actions โข Typed API Routes โข Environment Variables โข Generic Handlers
// โ Pattern 1: Typed Route Parameters// app/posts/[id]/page.tsxinterface PageProps {params: {id: string;};}export default function PostPage({ params }: PageProps) {return <div>Post ID: {params.id}</div>;}// โ Pattern 2: Typed Search Paramsinterface SearchPageProps {searchParams: {q?: string;page?: string;category?: string;};}export default function SearchPage({ searchParams }: SearchPageProps) {const query = searchParams.q || '';const page = parseInt(searchParams.page || '1');return <div>Search: {query}, Page: {page}</div>;}// โ Pattern 3: Typed Layout Propsinterface LayoutProps {children: React.ReactNode;params: {slug: string;};}export default function BlogLayout({ children, params }: LayoutProps) {return (<div><h1>Blog: {params.slug}</h1>{children}</div>);}// โ Pattern 4: Typed Metadataimport type { Metadata } from 'next';export const metadata: Metadata = {title: 'My Page',description: 'Page description',};// โ Pattern 5: Typed Server Actions'use server';type ActionResult<T> =| { success: true; data: T }| { success: false; error: string };export async function createPost(formData: FormData): Promise<ActionResult<{ id: string }>> {try {const post = await db.posts.create({title: formData.get('title') as string,});return { success: true, data: { id: post.id } };} catch (error) {return {success: false,error: error instanceof Error ? error.message : 'Unknown error',};}}// โ Pattern 6: Typed API Routesimport { NextRequest, NextResponse } from 'next/server';interface ApiResponse<T> {data?: T;error?: string;}export async function GET(request: NextRequest): Promise<NextResponse<ApiResponse<User[]>>> {try {const users = await getUsers();return NextResponse.json({ data: users });} catch (error) {return NextResponse.json({ error: 'Failed to fetch users' },{ status: 500 });}}// โ Pattern 7: Type-safe Environment Variables// env.d.tsdeclare namespace NodeJS {interface ProcessEnv {DATABASE_URL: string;NEXTAUTH_SECRET: string;NEXTAUTH_URL: string;NEXT_PUBLIC_API_URL: string;}}// โ Pattern 8: Shared Types// types/index.tsexport interface Post {id: string;title: string;content: string;publishedAt: Date;author: User;}export interface User {id: string;name: string;email: string;}// โ Pattern 9: Typed Dynamic Importsconst Component = dynamic<{ userId: string }>(() => import('./components/UserProfile'),{loading: () => <div>Loading...</div>,});// โ Pattern 10: Generic API Handlertype HandlerFunction<T = any> = (request: NextRequest,context?: any) => Promise<NextResponse<T>>;function createApiHandler<T>(handler: HandlerFunction<T>): HandlerFunction<T> {return async (request, context) => {try {return await handler(request, context);} catch (error) {return NextResponse.json({ error: 'Internal server error' },{ status: 500 });}};}export const GET = createApiHandler(async (request) => {const data = await fetchData();return NextResponse.json({ data });});
๐ญ 11. Production-Ready Patterns
Time to ship to production like a pro! ๐ข Error boundaries that don't leave users stranded, monitoring that catches issues before they escalate, logging that tells the full story, and config that scales. This is what separates hobby projects from production apps! ๐ผ
๐ด Impact: CRITICAL โ Production patterns are the difference between a toy and a real product โ skip these at your own peril! โ ๏ธ
๐ In this section: Error Boundaries โข Monitoring & Analytics โข Environment Config โข Feature Flags โข Health Checks โข Rate Limiting โข Security Headers
// โ Pattern 1: Error Boundaries// app/error.tsx'use client';export default function Error({error,reset,}: {error: Error & { digest?: string };reset: () => void;}) {// Log error to monitoring serviceuseEffect(() => {console.error('Error:', error);// Send to error tracking service (Sentry, etc.)logErrorToService(error);}, [error]);return (<div><h2>Something went wrong!</h2><button onClick={() => reset()}>Try again</button></div>);}// โ Pattern 2: Global Error Handler// app/global-error.tsx'use client';export default function GlobalError({error,reset,}: {error: Error & { digest?: string };reset: () => void;}) {return (<html><body><h2>Something went wrong!</h2><button onClick={() => reset()}>Try again</button></body></html>);}// โ Pattern 3: Monitoring & Analytics// lib/monitoring.tsexport function trackPageView(url: string) {if (typeof window !== 'undefined') {// Google Analytics, Plausible, etc.window.gtag?.('config', 'GA_MEASUREMENT_ID', {page_path: url,});}}export function trackEvent(name: string, params?: Record<string, any>) {if (typeof window !== 'undefined') {window.gtag?.('event', name, params);}}// โ Pattern 4: Environment Configuration// lib/config.tsexport const config = {app: {name: process.env.NEXT_PUBLIC_APP_NAME || 'My App',url: process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000',},api: {baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001',},features: {analytics: process.env.NEXT_PUBLIC_ENABLE_ANALYTICS === 'true',maintenance: process.env.NEXT_PUBLIC_MAINTENANCE_MODE === 'true',},};// โ Pattern 5: Maintenance Mode// middleware.tsexport function middleware(request: NextRequest) {if (process.env.NEXT_PUBLIC_MAINTENANCE_MODE === 'true') {if (!request.nextUrl.pathname.startsWith('/maintenance')) {return NextResponse.rewrite(new URL('/maintenance', request.url));}}return NextResponse.next();}// โ Pattern 6: Feature Flags// lib/features.tsexport const features = {newDashboard: process.env.NEXT_PUBLIC_FEATURE_NEW_DASHBOARD === 'true',betaFeatures: process.env.NEXT_PUBLIC_BETA_FEATURES === 'true',};export function isFeatureEnabled(feature: keyof typeof features): boolean {return features[feature] || false;}// โ Pattern 7: Logging Utility// lib/logger.tstype LogLevel = 'info' | 'warn' | 'error';export function logger(level: LogLevel, message: string, data?: any) {const timestamp = new Date().toISOString();const logEntry = {timestamp,level,message,...(data && { data }),};// Console loggingconsole[level](logEntry);// Send to logging service in productionif (process.env.NODE_ENV === 'production') {fetch('/api/logs', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify(logEntry),}).catch(() => {// Fail silently if logging fails});}}// โ Pattern 8: Health Check Endpoint// app/api/health/route.tsexport async function GET() {try {// Check database connectionawait db.$queryRaw`SELECT 1`;// Check external servicesconst externalServiceStatus = await checkExternalService();return NextResponse.json({status: 'healthy',timestamp: new Date().toISOString(),services: {database: 'healthy',external: externalServiceStatus,},});} catch (error) {return NextResponse.json({status: 'unhealthy',error: error instanceof Error ? error.message : 'Unknown error',},{ status: 503 });}}// โ Pattern 9: Rate Limiting// lib/rate-limit.tsimport { LRUCache } from 'lru-cache';const rateLimit = new LRUCache({max: 500,ttl: 60000, // 1 minute});export function rateLimitMiddleware(identifier: string, limit: number = 10) {const count = rateLimit.get(identifier) as number | undefined;if (count === undefined) {rateLimit.set(identifier, 1);return { allowed: true, remaining: limit - 1 };}if (count >= limit) {return { allowed: false, remaining: 0 };}rateLimit.set(identifier, count + 1);return { allowed: true, remaining: limit - count - 1 };}// โ Pattern 10: Security Headers// next.config.jsmodule.exports = {async headers() {return [{source: '/:path*',headers: [{key: 'X-DNS-Prefetch-Control',value: 'on',},{key: 'Strict-Transport-Security',value: 'max-age=63072000; includeSubDomains; preload',},{key: 'X-Frame-Options',value: 'SAMEORIGIN',},{key: 'X-Content-Type-Options',value: 'nosniff',},{key: 'Referrer-Policy',value: 'strict-origin-when-cross-origin',},],},];},};// โ Pattern 11: Database Connection Pooling// lib/db.tsimport { PrismaClient } from '@prisma/client';const globalForPrisma = globalThis as unknown as {prisma: PrismaClient | undefined;};export const db =globalForPrisma.prisma ??new PrismaClient({log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],});if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;// โ Pattern 12: Optimized Build Configuration// next.config.jsmodule.exports = {// Production optimizationscompress: true,poweredByHeader: false,// Bundle optimizationexperimental: {optimizePackageImports: ['@mui/material', 'lodash'],},// Webpack optimizationswebpack: (config, { isServer }) => {if (!isServer) {config.optimization = {...config.optimization,splitChunks: {chunks: 'all',cacheGroups: {default: false,vendors: false,// Vendor chunkvendor: {name: 'vendor',chunks: 'all',test: /node_modules/,},},},};}return config;},};
๐ 12. Real-World Application Examples
Theory is great, but real code is better! ๐ฅ Here are complete, copy-paste-ready examples straight from production: e-commerce pages, dashboards with live data, blog systems with comments, and auth flows. This is where all the patterns come together! ๐งฉโจ
๐ต Impact: MEDIUM โ Real-world examples bridge the gap between learning and building โ use these as starting points for your own projects! ๐
๐ In this section: E-commerce Product Page โข Real-time Dashboard โข Blog with Comments โข Authentication Flow
// โ Example 1: E-commerce Product Page// app/products/[id]/page.tsximport { notFound } from 'next/navigation';import Image from 'next/image';import { Suspense } from 'react';import AddToCartButton from './components/AddToCartButton';import ProductReviews from './components/ProductReviews';async function getProduct(id: string) {const res = await fetch(`https://api.example.com/products/\${id}`, {next: { revalidate: 3600 },});if (!res.ok) {return null;}return res.json();}export async function generateMetadata({params,}: {params: { id: string };}) {const product = await getProduct(params.id);if (!product) {return { title: 'Product Not Found' };}return {title: product.name,description: product.description,openGraph: {images: [product.image],},};}export default async function ProductPage({params,}: {params: { id: string };}) {const product = await getProduct(params.id);if (!product) {notFound();}return (<div className="product-page"><div className="product-images"><Imagesrc={product.image}alt={product.name}width={600}height={600}priority/></div><div className="product-info"><h1>{product.name}</h1><p className="price">$\${product.price}</p><p>{product.description}</p><Suspense fallback={<div>Loading...</div>}><AddToCartButton productId={product.id} /></Suspense><Suspense fallback={<ReviewsSkeleton />}><ProductReviews productId={product.id} /></Suspense></div></div>);}// โ Example 2: Dashboard with Real-time Data// app/dashboard/page.tsximport { Suspense } from 'react';import RevenueChart from './components/RevenueChart';import UserStats from './components/UserStats';import RecentActivity from './components/RecentActivity';export default function DashboardPage() {return (<div className="dashboard"><h1>Dashboard</h1><div className="stats-grid"><Suspense fallback={<StatSkeleton />}><RevenueChart /></Suspense><Suspense fallback={<StatSkeleton />}><UserStats /></Suspense></div><Suspense fallback={<ActivitySkeleton />}><RecentActivity /></Suspense></div>);}// app/dashboard/components/RevenueChart.tsxasync function getRevenue() {const res = await fetch('https://api.example.com/revenue', {cache: 'no-store', // Always fresh});return res.json();}export default async function RevenueChart() {const revenue = await getRevenue();return (<div><h2>Revenue</h2><Chart data={revenue} /></div>);}// โ Example 3: Blog with Comments// app/blog/[slug]/page.tsximport { Suspense } from 'react';import { notFound } from 'next/navigation';import PostContent from './components/PostContent';import CommentsSection from './components/CommentsSection';async function getPost(slug: string) {const res = await fetch(`https://api.example.com/posts/\${slug}`, {next: { revalidate: 3600 },});if (!res.ok) {return null;}return res.json();}export default async function BlogPostPage({params,}: {params: { slug: string };}) {const post = await getPost(params.slug);if (!post) {notFound();}return (<article><PostContent post={post} /><Suspense fallback={<CommentsSkeleton />}><CommentsSection postId={post.id} /></Suspense></article>);}// โ Example 4: Authentication Flow// app/login/page.tsximport { redirect } from 'next/navigation';import { auth } from '@/lib/auth';import LoginForm from './components/LoginForm';export default async function LoginPage() {const session = await auth();// Redirect if already authenticatedif (session) {redirect('/dashboard');}return (<div className="login-page"><h1>Login</h1><LoginForm /></div>);}// app/login/components/LoginForm.tsx'use client';import { useFormState } from 'react-dom';import { login } from '../actions';export default function LoginForm() {const [state, formAction] = useFormState(login, null);return (<form action={formAction}><input name="email" type="email" required /><input name="password" type="password" required />{state?.error && (<div className="error">{state.error}</div>)}<button type="submit">Login</button></form>);}// app/login/actions.ts'use server';import { signIn } from '@/lib/auth';import { redirect } from 'next/navigation';export async function login(prevState: any, formData: FormData) {const email = formData.get('email') as string;const password = formData.get('password') as string;try {await signIn('credentials', {email,password,redirect: false,});redirect('/dashboard');} catch (error) {return {error: 'Invalid credentials',};}}
๐งช 13. Senior Next.js Tooling & Testing Playbook
This is the senior engineer's secret playbook! ๐ Edge runtime tricks, cache-busting strategies, Zod-validated server actions, OpenTelemetry observability, and a full testing stack with Playwright + Vitest + MSW. If you're reading this, you're leveling up to Staff Engineer territory! ๐ ๐
๐ Impact: HIGH โ Senior-level tooling and testing is what makes codebases maintainable for years โ invest here and thank yourself later! ๐
๐ In this section: Edge Runtime Patterns โข Cache Tag Revalidation โข Zod Server Actions โข OpenTelemetry โข Playwright E2E โข Vitest + MSW
// โ Edge Runtime + Geo A/B with Middleware// middleware.tsimport { NextResponse, type NextRequest } from 'next/server';export const config = {matcher: ['/checkout/:path*', '/promo'],};export function middleware(req: NextRequest) {const country = req.geo?.country || 'US';const url = req.nextUrl.clone();// Send some countries to lightweight edge experienceif (country === 'BR') {url.pathname = '/promo/latam';return NextResponse.rewrite(url);}// Security headers at the edgeconst res = NextResponse.next();res.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');res.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');return res;}// โ Cache Strategy + Tag Revalidation// app/api/products/route.tsimport { revalidateTag } from 'next/cache';export const revalidate = 3600; // 1h ISR for this route segmentexport async function GET() {const products = await fetchProducts(); // server-only callreturn Response.json({ data: products }, { status: 200, headers: { 'Cache-Control': 's-maxage=3600' } });}export async function POST(request: Request) {const body = await request.json();await createProduct(body);revalidateTag('products'); // bust all pages/components using this tagreturn Response.json({ ok: true }, { status: 201 });}// โ Server Action with Zod validation + typed result// app/(admin)/products/actions.ts'use server';import { z } from 'zod';import { revalidatePath, revalidateTag } from 'next/cache';const ProductSchema = z.object({name: z.string().min(2),price: z.number().positive(),tags: z.array(z.string()).optional(),});export async function createProductAction(_prev: unknown, formData: FormData) {const parsed = ProductSchema.safeParse({name: formData.get('name'),price: Number(formData.get('price')),tags: formData.getAll('tags'),});if (!parsed.success) {return { success: false, errors: parsed.error.flatten().fieldErrors };}await db.product.create({ data: parsed.data });revalidateTag('products');revalidatePath('/dashboard/products');return { success: true };}// โ Observability hook (instrumentation)// instrumentation.tsimport type { OpenTelemetryConfig } from 'next';export const registerOTel = (): OpenTelemetryConfig => ({resourceDetectors: [],instrumentations: {http: { enabled: true },fetch: { enabled: true },},});// โ Type-safe env (server + client)// env.d.tsdeclare namespace NodeJS {interface ProcessEnv {NEXT_PUBLIC_APP_URL: string;NEXT_PUBLIC_ENABLE_ANALYTICS?: 'true' | 'false';DATABASE_URL: string;SENTRY_DSN?: string;}}// โ Playwright E2E with seeded data// playwright.config.tsimport { defineConfig } from '@playwright/test';export default defineConfig({use: {baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',screenshot: 'only-on-failure',trace: 'retain-on-failure',video: 'retain-on-failure',},webServer: {command: 'pnpm dev',port: 3000,reuseExistingServer: !process.env.CI,},});// tests/e2e/cart.spec.tsimport { test, expect } from '@playwright/test';test('adds item to cart', async ({ page }) => {await page.goto('/products');await page.getByRole('button', { name: 'Add to cart' }).first().click();await page.goto('/cart');await expect(page.getByText('Subtotal')).toBeVisible();});// โ Vitest + MSW for server/client units// tests/product.test.tsimport { describe, it, expect } from 'vitest';import { http, HttpResponse } from 'msw';import { setupServer } from 'msw/node';import { getProducts } from '../app/api/products/data';const server = setupServer(http.get('https://api.example.com/products', () => HttpResponse.json([{ id: 1, name: 'Test' }])),);beforeAll(() => server.listen());afterAll(() => server.close());afterEach(() => server.resetHandlers());describe('getProducts', () => {it('returns data', async () => {const products = await getProducts();expect(products[0].name).toBe('Test');});});