Introduction to Next.js 14 App Router
Next.js 14 introduces significant architectural changes with the stable App Router, moving from a page-based to a component-based routing system. This shift enables better performance, improved developer experience, and more flexible data fetching patterns.
App Router vs Pages Router
- File-based routing with enhanced conventions
- Server Components by default for better performance
- Nested layouts and parallel routes
- Improved data fetching with async components
- Built-in loading and error states
- Streaming and Suspense integration
- Enhanced metadata API
Server Components Architecture
Server Components run exclusively on the server, sending minimal JavaScript to the client. This reduces bundle size and enables direct database access without API layers.
// Server Component - runs on server, no JavaScript sent to client
export default async function ProductPage({ params }: { params: { id: string } }) {
// Direct database access - secure and efficient
const product = await db.products.findUnique({
where: { id: params.id }
});
// No useState, useEffect, or event handlers here
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
{/* Client Component for interactivity */}
<AddToCartButton productId={product.id} />
</div>
);
}
// Client Component - marked with 'use client'
'use client';
export function AddToCartButton({ productId }: { productId: string }) {
const [loading, setLoading] = useState(false);
const handleClick = async () => {
setLoading(true);
await addToCart(productId);
setLoading(false);
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Adding...' : 'Add to Cart'}
</button>
);
}Data Fetching Patterns
App Router introduces new data fetching patterns that optimize for performance and user experience.
// Parallel data fetching
export default async function Dashboard() {
// Fetch multiple data sources in parallel
const [userData, orders, analytics] = await Promise.all([
fetchUserData(),
fetchOrders(),
fetchAnalytics(),
]);
return (
<div>
<UserProfile data={userData} />
<OrderList orders={orders} />
<AnalyticsChart data={analytics} />
</div>
);
}
// Sequential with loading states
export default function UserPage() {
return (
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
);
}
async function UserProfile() {
// This component will stream in when data is ready
const user = await fetchUser();
return <div>{user.name}</div>;
}
// Using React Cache for deduplication
import { cache } from 'react';
const getUser = cache(async (id: string) => {
return db.users.findUnique({ where: { id } });
});Caching Strategies
Next.js 14 provides multiple caching layers that work together to optimize performance.
// Route Segment Config
export const revalidate = 3600; // Revalidate every hour
export const dynamic = 'force-static'; // Static generation
export const dynamicParams = true; // Allow dynamic params not in generateStaticParams
// fetch API options
async function getProduct(id: string) {
const res = await fetch(`https://api.example.com/products/${id}`, {
next: {
revalidate: 60, // ISR: Revalidate every minute
tags: ['products'], // For on-demand revalidation
},
cache: 'force-cache', // Default: Cache unless opted out
});
// or for dynamic data
const dynamicRes = await fetch(`https://api.example.com/live-data/${id}`, {
cache: 'no-store', // Don't cache - always fresh
});
}
// On-demand revalidation
export async function revalidateProducts() {
revalidateTag('products'); // Revalidate all fetch calls with 'products' tag
revalidatePath('/products'); // Revalidate specific path
}
// Time-based revalidation in layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<header>My App</header>
{children}
<footer>© 2024</footer>
</body>
</html>
);
}
export const revalidate = 60; // Revalidate entire layout every minuteStreaming and Suspense
Streaming allows sending UI in chunks as they become ready, improving perceived performance.
// Streaming with loading.js
export default function Dashboard() {
return (
<div>
<Suspense fallback={<CardSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
// loading.js file automatically wraps page in Suspense
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
<div className="space-y-4">
{[1, 2, 3].map(i => (
<div key={i} className="h-20 bg-gray-200 rounded"></div>
))}
</div>
</div>
);
}
// Streaming server actions
'use server';
export async function submitForm(formData: FormData) {
// Process form data
await db.forms.create({ data: { content: formData.get('content') } });
// Revalidate cache
revalidatePath('/forms');
// Return streamable data
return { success: true, message: 'Form submitted' };
}Metadata and SEO
Next.js 14 provides a powerful metadata API for managing SEO and social sharing cards.
// Static metadata
export const metadata: Metadata = {
title: 'My App',
description: 'A wonderful application',
openGraph: {
title: 'My App',
description: 'A wonderful application',
images: ['/og-image.png'],
},
twitter: {
card: 'summary_large_image',
title: 'My App',
description: 'A wonderful application',
images: ['/twitter-image.png'],
},
};
// Dynamic metadata
export async function generateMetadata({
params,
searchParams
}: {
params: { id: string };
searchParams: { [key: string]: string | string[] | undefined };
}): Promise<Metadata> {
const product = await fetchProduct(params.id);
return {
title: product.name,
description: product.description,
openGraph: {
images: [product.imageUrl],
},
};
}
// Viewport configuration
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#000000',
};
// JSON-LD structured data
export default function ProductPage({ product }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Product',
name: product.name,
image: product.imageUrl,
description: product.description,
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* Page content */}
</>
);
}Middleware and Authentication
Middleware runs before requests are completed, enabling authentication, redirects, and more.
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// Check authentication
const token = request.cookies.get('auth-token');
const isAuthPage = request.nextUrl.pathname.startsWith('/auth');
const isProtectedPage = request.nextUrl.pathname.startsWith('/dashboard');
// Redirect unauthenticated users from protected pages
if (isProtectedPage && !token) {
return NextResponse.redirect(new URL('/auth/login', request.url));
}
// Redirect authenticated users away from auth pages
if (isAuthPage && token) {
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// Add headers to all responses
const response = NextResponse.next();
response.headers.set('x-custom-header', 'my-value');
return response;
}
// Configuring middleware matcher
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};
// API route protection
export async function GET(request: NextRequest) {
const session = await getSession(request);
if (!session) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return NextResponse.json({ data: 'Protected data' });
}Performance Optimization
- Use server components for static content
- Implement proper caching strategies
- Optimize images with next/image
- Code split with dynamic imports
- Use React.memo for expensive client components
- Implement proper loading states
- Monitor Core Web Vitals
- Use next/script for third-party scripts
Production Deployment Checklist
# Environment variables
NEXT_PUBLIC_API_URL=https://api.example.com
DATABASE_URL=postgresql://...
# next.config.js optimizations
/** @type {import('next').NextConfig} */
const nextConfig = {
compress: true,
poweredByHeader: false,
generateEtags: true,
swcMinify: true,
experimental: {
// Enable production features
serverActions: {
bodySizeLimit: '2mb',
},
},
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
remotePatterns: [
{
protocol: 'https',
hostname: '**.example.com',
},
],
},
};
module.exports = nextConfig;Common Pitfalls and Solutions
- Mixing server and client component patterns - solution: clearly separate concerns
- Over-fetching in server components - solution: use selective data fetching
- Ignoring loading states - solution: implement proper Suspense boundaries
- Poor caching strategy - solution: analyze data update frequency
- Large bundle sizes - solution: analyze with @next/bundle-analyzer
- Missing error boundaries - solution: implement error.js files