Web Development · Next.js
Next.js 15 & 16: The Complete App Router Guide
Master Next.js App Router with this comprehensive guide covering Server Components, Partial Pre-rendering, typed routes, and the latest features in Next.js 15 and 16.
Anurag Verma
8 min read
Sponsored
Next.js has evolved into the de facto framework for React applications. With Next.js 15 and 16 now available, the App Router has matured into a powerful, production-ready architecture. Here’s your complete guide.
Next.js App Router provides a modern file-based routing system with Server Components
Evolution Timeline
- October 2024: Next.js 15 released with Turbopack, React 19 support
- March 2025: Next.js 15.5 with typed routes
- October 2025: Next.js 16 with Cache Components
Understanding the App Router
The App Router is a file-system based router that leverages React’s latest features:
app/
├── layout.tsx # Root layout
├── page.tsx # Home page (/)
├── loading.tsx # Loading UI
├── error.tsx # Error UI
├── not-found.tsx # 404 page
│
├── about/
│ └── page.tsx # /about
│
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/
│ └── page.tsx # /blog/:slug
│
└── api/
└── route.ts # API route
Key Concepts
| Concept | Description |
|---|---|
| Server Components | Default, run on server only |
| Client Components | Interactive, marked with ‘use client’ |
| Layouts | Shared UI that preserves state |
| Loading States | Automatic Suspense boundaries |
| Error Handling | Granular error boundaries |
Server Components by Default
Every component in the App Router is a Server Component by default:
// app/products/page.tsx
// This is a Server Component - no 'use client' directive
import { db } from '@/lib/database';
export default async function ProductsPage() {
// Direct database access - no API layer needed!
const products = await db.products.findMany({
orderBy: { createdAt: 'desc' },
});
return (
<main>
<h1>Products</h1>
<ProductGrid products={products} />
</main>
);
}
Benefits of Server Components
Server Component Benefits
├── Zero JavaScript sent to client
├── Direct database/API access
├── Smaller bundle sizes
├── Better initial page load
├── SEO-friendly by default
└── Secure - secrets stay on server
Server Components render on the server, Client Components handle interactivity
Data Fetching Patterns
Pattern 1: Async Server Components
// app/users/page.tsx
async function UsersPage() {
const users = await fetch('https://api.example.com/users', {
next: { revalidate: 3600 } // Cache for 1 hour
}).then(res => res.json());
return <UserList users={users} />;
}
Pattern 2: Parallel Data Fetching
// app/dashboard/page.tsx
async function DashboardPage() {
// Fetch in parallel - not waterfall!
const [user, stats, notifications] = await Promise.all([
getUser(),
getStats(),
getNotifications(),
]);
return (
<Dashboard
user={user}
stats={stats}
notifications={notifications}
/>
);
}
Pattern 3: Streaming with Suspense
// app/product/[id]/page.tsx
import { Suspense } from 'react';
export default function ProductPage({ params }) {
return (
<div>
{/* Renders immediately */}
<ProductHeader id={params.id} />
{/* Streams in when ready */}
<Suspense fallback={<ReviewsSkeleton />}>
<ProductReviews id={params.id} />
</Suspense>
{/* Streams in when ready */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations id={params.id} />
</Suspense>
</div>
);
}
Next.js 15.5: Typed Routes
One of the most requested features - compile-time type safety for routes:
// Enable in next.config.js
module.exports = {
experimental: {
typedRoutes: true,
},
};
// Now TypeScript catches invalid routes!
import Link from 'next/link';
// ✅ Valid - type-checked
<Link href="/about">About</Link>
<Link href="/blog/my-post">Blog Post</Link>
<Link href="/products/123">Product</Link>
// ❌ Error - route doesn't exist
<Link href="/invalid-page">Invalid</Link>
// TypeScript Error: Type '"/invalid-page"' is not assignable
Dynamic Route Types
// app/blog/[slug]/page.tsx generates:
type BlogSlugParams = {
slug: string;
};
// app/products/[...categories]/page.tsx generates:
type ProductCategoriesParams = {
categories: string[];
};
// Full type safety in components
export default function BlogPost({ params }: { params: BlogSlugParams }) {
return <article>{params.slug}</article>;
}
Next.js 16: Cache Components
Next.js 16 introduces Cache Components - a new programming model for caching:
// app/products/page.tsx
import { cache } from 'next/cache';
// Define cached data fetching
const getProducts = cache(async () => {
return await db.products.findMany();
}, {
tags: ['products'],
revalidate: 3600, // 1 hour
});
export default async function ProductsPage() {
const products = await getProducts();
return <ProductGrid products={products} />;
}
// Revalidate from Server Action
'use server'
import { revalidateTag } from 'next/cache';
export async function addProduct(data: FormData) {
await db.products.create({ data });
revalidateTag('products'); // Invalidate cache
}
Server Actions Deep Dive
Server Actions simplify data mutations:
// app/actions/contact.ts
'use server'
import { z } from 'zod';
import { db } from '@/lib/database';
const ContactSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContact(formData: FormData) {
const validated = ContactSchema.parse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
});
await db.contacts.create({ data: validated });
return { success: true };
}
// app/contact/page.tsx
import { submitContact } from '@/actions/contact';
export default function ContactPage() {
return (
<form action={submitContact}>
<input name="name" required />
<input name="email" type="email" required />
<textarea name="message" required />
<SubmitButton />
</form>
);
}
// Client Component for form state
'use client'
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Sending...' : 'Send Message'}
</button>
);
}
Server Actions handle form submissions without separate API routes
Partial Pre-rendering (PPR)
PPR combines static and dynamic rendering in a single route:
// app/product/[id]/page.tsx
export const experimental_ppr = true;
export default async function ProductPage({ params }) {
return (
<div>
{/* Static - pre-rendered at build time */}
<Header />
<Navigation />
{/* Dynamic - rendered at request time */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails id={params.id} />
</Suspense>
{/* Dynamic - rendered at request time */}
<Suspense fallback={<PricingSkeleton />}>
<DynamicPricing id={params.id} />
</Suspense>
{/* Static - pre-rendered */}
<Footer />
</div>
);
}
How PPR Works
Request Flow with PPR:
1. Instant static shell from CDN
2. Stream dynamic content as it resolves
3. Progressive enhancement
Result:
├── TTFB: ~50ms (static shell)
├── FCP: ~100ms (visible content)
└── Complete: Varies (streaming)
Route Handlers (API Routes)
// app/api/products/route.ts
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get('category');
const products = await db.products.findMany({
where: category ? { category } : undefined,
});
return NextResponse.json(products);
}
export async function POST(request: Request) {
const body = await request.json();
const product = await db.products.create({
data: body,
});
return NextResponse.json(product, { status: 201 });
}
Dynamic Route Handlers
// app/api/products/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const product = await db.products.findUnique({
where: { id: params.id },
});
if (!product) {
return NextResponse.json(
{ error: 'Not found' },
{ status: 404 }
);
}
return NextResponse.json(product);
}
Middleware
Handle requests before they reach your routes:
// 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');
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// Add headers
const response = NextResponse.next();
response.headers.set('x-custom-header', 'value');
return response;
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
Turbopack: Next-Gen Bundler
Next.js 15 brings Turbopack as the default development bundler:
# Automatic in Next.js 15+
npm run dev
# Explicit in older versions
next dev --turbo
Performance Comparison
| Metric | Webpack | Turbopack | Improvement |
|---|---|---|---|
| Cold Start | 12s | 1.8s | 6.7x faster |
| HMR (small) | 500ms | 50ms | 10x faster |
| HMR (large) | 2s | 200ms | 10x faster |
Best Practices
1. Colocate Data Fetching
// Good: Data fetching in the component that needs it
async function ProductCard({ id }) {
const product = await getProduct(id);
return <div>{product.name}</div>;
}
// Avoid: Prop drilling from parent
function ProductCard({ product }) {
return <div>{product.name}</div>;
}
2. Use Loading States
// app/products/loading.tsx
export default function Loading() {
return (
<div className="grid grid-cols-3 gap-4">
{[...Array(6)].map((_, i) => (
<div key={i} className="animate-pulse h-48 bg-gray-200" />
))}
</div>
);
}
3. Error Boundaries
// app/products/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
4. Metadata API
// app/blog/[slug]/page.tsx
import { Metadata } from 'next';
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [post.image],
},
};
}
Migration Checklist
Migrating to App Router:
□ Move pages to app/ directory
□ Convert _app.tsx to layout.tsx
□ Convert _document.tsx to layout.tsx
□ Update data fetching (getServerSideProps → async components)
□ Update API routes to route handlers
□ Add 'use client' to interactive components
□ Update metadata approach
□ Test all routes
Resources
Building a Next.js application? Contact CODERCOPS for expert Next.js development services.
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored