Comprehensive Next.js 16 App Router patterns, Server vs Client Components decision tree, route handlers, Server Actions, middleware, streaming with Suspense, parallel routes, intercepting routes, and advanced routing patterns.
Comprehensive Next.js 16 App Router patterns, Server vs Client Components decision tree, route handlers, Server Actions, middleware, streaming with Suspense, parallel routes, intercepting routes, and advanced routing patterns.
This skill provides comprehensive guidance for building production-ready Next.js 16 applications using the App Router. It covers Server Components, Client Components, routing patterns, data fetching, Server Actions, middleware, and advanced features like parallel routes and intercepting routes.
// app/products/page.tsx
import { prisma } from '@/lib/prisma'
export default async function ProductsPage() {
const products = await prisma.product.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
})
return (
<div>
<h1>Products</h1>
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
)
}
'use client'
import { useState } from 'react'
export function ProductSearch() {
const [query, setQuery] = useState('')
return (
<div>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products..."
/>
</div>
)
}
Route handlers allow you to create custom request handlers for specific routes using the Web Request and Response APIs.
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(request: NextRequest) {
const users = await prisma.user.findMany()
return NextResponse.json(users)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const user = await prisma.user.create({
data: body,
})
return NextResponse.json(user, { status: 201 })
}
// app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const user = await prisma.user.findUnique({
where: { id: params.id },
})
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
return NextResponse.json(user)
}
Server Actions are async functions that run on the server. They can be called from Server Components, Client Components, or other Server Actions.
// app/actions/user.ts
'use server'
import { prisma } from '@/lib/prisma'
import { revalidatePath } from 'next/cache'
export async function createUser(formData: FormData) {
const name = formData.get('name') as string
const email = formData.get('email') as string
const user = await prisma.user.create({
data: { name, email },
})
revalidatePath('/users')
return user
}
// app/users/create/page.tsx
import { createUser } from '@/app/actions/user'
export default function CreateUserPage() {
return (
<form action={createUser}>
<input name="name" type="text" required />
<input name="email" type="email" required />
<button type="submit">Create User</button>
</form>
)
}
'use client'
import { useTransition } from 'react'
import { createUser } from '@/app/actions/user'
export function CreateUserForm() {
const [isPending, startTransition] = useTransition()
async function handleSubmit(formData: FormData) {
startTransition(async () => {
await createUser(formData)
})
}
return (
<form action={handleSubmit}>
<input name="name" type="text" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Creating...' : 'Create User'}
</button>
</form>
)
}
Middleware runs before a request is completed and can modify the response by rewriting, redirecting, modifying headers, or setting cookies.
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-token')
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*'],
}
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const response = NextResponse.next()
// Add custom header
response.headers.set('x-custom-header', 'value')
// Set cookie
response.cookies.set('last-visited', new Date().toISOString())
return response
}
Streaming allows you to progressively render UI as data becomes available.
// app/products/page.tsx
import { Suspense } from 'react'
import { ProductList } from '@/components/ProductList'
import { ProductSkeleton } from '@/components/ProductSkeleton'
export default function ProductsPage() {
return (
<div>
<h1>Products</h1>
<Suspense fallback={<ProductSkeleton />}>
<ProductList />
</Suspense>
</div>
)
}
// app/dashboard/page.tsx
import { Suspense } from 'react'
import { UserProfile } from '@/components/UserProfile'
import { RecentOrders } from '@/components/RecentOrders'
import { Stats } from '@/components/Stats'
export default function DashboardPage() {
return (
<div>
<Suspense fallback={<div>Loading profile...</div>}>
<UserProfile />
</Suspense>
<Suspense fallback={<div>Loading orders...</div>}>
<RecentOrders />
</Suspense>
<Suspense fallback={<div>Loading stats...</div>}>
<Stats />
</Suspense>
</div>
)
}
Parallel routes allow you to simultaneously render multiple pages in the same layout that can be navigated independently.
// app/dashboard/@analytics/page.tsx
export default function AnalyticsPage() {
return <div>Analytics Content</div>
}
// app/dashboard/@team/page.tsx
export default function TeamPage() {
return <div>Team Content</div>
}
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div>
{children}
<div className="grid grid-cols-2">
<div>{analytics}</div>
<div>{team}</div>
</div>
</div>
)
}
Intercepting routes allow you to load a route while keeping the context of the current page in the background.
// app/@modal/(.)photo/[id]/page.tsx
import { Modal } from '@/components/Modal'
import { Photo } from '@/components/Photo'
export default function PhotoModal({
params,
}: {
params: { id: string }
}) {
return (
<Modal>
<Photo id={params.id} />
</Modal>
)
}
// app/photo/[id]/page.tsx
import { Photo } from '@/components/Photo'
export default function PhotoPage({ params }: { params: { id: string } }) {
return <Photo id={params.id} />
}
Route groups organize routes without affecting the URL structure.
app/
(marketing)/
about/
page.tsx
contact/
page.tsx
(shop)/
products/
page.tsx
cart/
page.tsx
// app/(marketing)/layout.tsx
export default function MarketingLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="marketing-layout">
<header>Marketing Header</header>
{children}
<footer>Marketing Footer</footer>
</div>
)
}
// app/products/page.tsx
import { prisma } from '@/lib/prisma'
export default async function ProductsPage() {
const products = await prisma.product.findMany()
return (
<div>
{products.map((product) => (
<div key={product.id}>{product.name}</div>
))}
</div>
)
}
// app/products/page.tsx
import { prisma } from '@/lib/prisma'
export const revalidate = 3600 // Revalidate every hour
export default async function ProductsPage() {
const products = await prisma.product.findMany()
return <div>...</div>
}
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const path = request.nextUrl.searchParams.get('path')
if (path) {
revalidatePath(path)
return Response.json({ revalidated: true })
}
return Response.json({ revalidated: false })
}
Use Edge Runtime for routes that need low latency and can run at the edge.
// app/api/hello/route.ts
export const runtime = 'edge'
export async function GET() {
return Response.json({ message: 'Hello from Edge!' })
}
This skill should be used when strict adherence to the defined process is required.
Category:other