/blog route
β βββ [slug]/
β βββ page.js # /blog/[slug] dynamic route
βββ dashboard/
βββ layout.js # Dashboard layout
βββ page.js # /dashboard route
βββ settings/
βββ page.js # /dashboard/settings route
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return <h1>Post: {params.slug}</h1>
}
// Generate static params at build time
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map((post) => ({
slug: post.slug,
}))
}
// app/docs/[...slug]/page.js
export default function Docs({ params }) {
// params.slug will be an array: ['getting-started', 'installation']
return <div>Docs: {params.slug.join('/')}</div>
}
### Layouts and Templates
Layouts persist across route changes, preserving state and avoiding re-renders. Templates re-mount on navigation, ideal for animations or isolated state.
// app/layout.js
export const metadata = {
title: 'My App',
description: 'Welcome to my app',
}
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<header>
<nav>{/* Navigation /}</nav>
</header>
<main>{children}</main>
<footer>{/ Footer */}</footer>
</body>
</html>
)
}
// app/dashboard/layout.js
export default function DashboardLayout({ children }) {
return (
<div className="dashboard">
<aside>
<DashboardNav />
</aside>
<section>{children}</section>
</div>
)
}
// app/template.js
export default function Template({ children }) {
return <div className="animate-fade-in">{children}</div>
}
### Server and Client Components
Server Components run exclusively on the server, enabling direct database/API access and zero client bundle impact. Client Components use `'use client'` to enable interactivity. Composition bridges the two.
// app/posts/page.js - Server Component
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
cache: 'no-store', // Dynamic data
})
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
)
}
// app/page.js - Server Component
import ClientComponent from './ClientComponent'
async function getData() {
const data = await fetch('...')
return data.json()
}
export default async function Page() {
const data = await getData()
return (
<div>
<h1>Server-rendered content</h1>
<ClientComponent initialData={data} />
</div>
)
}
### Data Fetching Strategies
Next.js leverages the native `fetch` API with extended options for caching, revalidation, and parallel execution.
// Cached by default
async function getStaticData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
// Opt out of caching
async function getDynamicData() {
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
return res.json()
}
// Revalidate every 60 seconds
async function getRevalidatedData() {
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
return res.json()
}
export default async function Page() {
// Fetch in parallel
const [posts, users] = await Promise.all([
getPosts(),
getUsers()
])
return (
<div>
<Posts data={posts} />
<Users data={users} />
</div>
)
}
export default async function Page() {
const user = await getUser()
const posts = await getUserPosts(user.id) // Waits for user
return <UserPosts user={user} posts={posts} />
}
### Route Handlers (API Routes)
Route handlers replace `pages/api` with colocated `route.js` files supporting standard HTTP methods and streaming responses.
// app/api/posts/route.js
export async function GET(request) {
const posts = await getPosts()
return Response.json(posts)
}
export async function POST(request) {
const body = await request.json()
const post = await createPost(body)
return Response.json(post, { status: 201 })
}
// app/api/posts/[id]/route.js
export async function GET(request, { params }) {
const post = await getPost(params.id)
if (!post) {
return Response.json({ error: 'Not found' }, { status: 404 })
}
return Response.json(post)
}
export async function PATCH(request, { params }) {
const body = await request.json()
const post = await updatePost(params.id, body)
return Response.json(post)
}
export async function DELETE(request, { params }) {
await deletePost(params.id)
return new Response(null, { status: 204 })
}
// app/api/protected/route.js
import { auth } from '@/lib/auth'
export async function GET(request) {
const session = await auth(request)
if (!session) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const data = await getProtectedData(session.userId)
return Response.json(data)
}
## Pitfall Guide
1. **Overusing `'use client'` Boundaries**: Placing the directive at the root or high-level components forces entire subtrees into the client bundle, negating server component benefits and increasing hydration time. Keep client boundaries as low as possible.
2. **Ignoring Data Fetching Waterfalls**: Sequential `await` calls block rendering. Use `Promise.all()` for independent requests or leverage React `Suspense` boundaries to stream partial UI while dependencies resolve.
3. **Misunderstanding Layout vs Template Re-render Behavior**: Layouts persist across navigation and preserve state; templates remount on every route change. Using a layout when you need animation/state reset (or vice versa) causes unexpected UI behavior.
4. **Improper Cache Configuration**: Default `fetch` caching is aggressive. Using `cache: 'no-store'` globally defeats static generation benefits, while omitting `next.revalidate` on dynamic data causes stale responses. Match cache strategy to data volatility.
5. **Route Handler Middleware Misplacement**: Implementing auth/validation inside individual route handlers creates duplication and security gaps. Use Next.js Middleware (`middleware.ts`) for cross-cutting concerns like authentication, rate limiting, and locale routing.
6. **Forgetting `generateStaticParams` in Production**: Dynamic routes without `generateStaticParams` fall back to server-side rendering on every request, increasing cold start latency and compute costs. Always pre-generate known paths.
7. **Crossing Server/Client Boundaries Incorrectly**: Passing functions, promises, or React refs from Server to Client Components throws serialization errors. Only pass serializable props (strings, numbers, plain objects, arrays) across the boundary.
## Deliverables
- **π Next.js App Router Architecture Blueprint**: Visual mapping of `app/` directory structure, server/client component boundary placement, data flow diagrams, and streaming suspense boundaries for production-grade applications.
- **β
Pre-Deployment Validation Checklist**:
- [ ] Verify `'use client'` directives are scoped to minimal interactive components
- [ ] Audit `fetch` cache strategies (`force-cache`, `no-store`, `next.revalidate`)
- [ ] Confirm `generateStaticParams` covers all dynamic route permutations
- [ ] Validate route handler HTTP methods and error responses (401/404/500)
- [ ] Test streaming fallbacks with `<Suspense>` and `loading.js`
- [ ] Run bundle analyzer to ensure client payload < 150KB
- **βοΈ Configuration Templates**:
- `next.config.js` with `experimental.serverComponentsExternalPackages` and caching headers
- `middleware.ts` for auth/redirect routing
- `tsconfig.json` path aliases and strict type checking presets for RSC compatibility