nextjs-16
Build Next.js 16 apps with Turbopack, Cache Components, proxy.ts, Server Actions, React 19.2 View Transitions, and App Router patterns. Covers streaming SSR, ISR, error boundaries, and advanced routing.
Next.js 16 Development Skill
You are a Next.js 16 expert. Build modern web applications using Turbopack, Cache Components, App Router, React Server Components, and streaming SSR.
Core Principles
- Server Components by default. Client Components only when needed (interactivity, hooks, browser APIs).
- Streaming and Suspense. Load UI progressively, never block entire page.
- Cache Components over implicit caching. Use
"use cache"directive explicitly. All dynamic code runs at request time by default. - Type safety. TypeScript strict mode. All params/searchParams are async (
await params). - proxy.ts over middleware.ts. Use
proxy.tsfor request interception (Node.js runtime).middleware.tsis deprecated.
App Router Structure
BAD - Pages Router patterns:
// pages/api/users.ts - REMOVED IN 16
export default function handler(req, res) {
res.json({ users: [] })
}
GOOD - App Router with colocation:
// app/dashboard/page.tsx
export default function DashboardPage() {
return <div>Dashboard</div>
}
// app/api/users/route.ts
export async function GET() {
return Response.json({ users: [] })
}
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return <div className="dashboard-wrapper">{children}</div>
}
Cache Components (New in 16)
BAD - Relying on implicit caching (removed in 16):
// experimental.ppr flag - REMOVED
// next.config: { experimental: { ppr: true } } - REMOVED
export default async function PostsPage() {
const res = await fetch('https://api.example.com/posts', {
cache: 'force-cache' // Old implicit caching
})
const posts = await res.json()
return <PostList posts={posts} />
}
GOOD - Explicit Cache Components with "use cache":
// next.config.ts
const nextConfig = {
cacheComponents: true,
}
export default nextConfig
// app/posts/page.tsx
"use cache"
export default async function PostsPage() {
const posts = await db.posts.findMany()
return <PostList posts={posts} />
}
GOOD - Cache at function level:
async function getProducts() {
"use cache"
return await db.products.findMany()
}
export default async function ProductsPage() {
const products = await getProducts()
return <ProductGrid products={products} />
}
Proxy (Replaces Middleware)
BAD - Using deprecated middleware.ts:
// middleware.ts - DEPRECATED, will be removed
export function middleware(request: NextRequest) {
return NextResponse.redirect(new URL('/home', request.url))
}
GOOD - Using proxy.ts (Node.js runtime):
// proxy.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export default function proxy(request: NextRequest) {
const token = request.cookies.get('token')?.value
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url))
}
return NextResponse.next()
}
export const config = {
matcher: ['/dashboard/:path*', '/admin/:path*']
}
Server Components vs Client Components
BAD - Using Client Component unnecessarily:
'use client'
export default function UserList({ users }: { users: User[] }) {
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
GOOD - Server Component with async params (required in 16):
// app/users/page.tsx
export default async function UsersPage() {
const users = await db.users.findMany()
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}
// app/users/[id]/page.tsx - params are async in 16
export default async function UserPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const user = await db.users.findUnique({ where: { id } })
return <div>{user.name}</div>
}
Streaming SSR with Suspense
BAD - Blocking entire page load:
export default async function Dashboard() {
const user = await fetchUser()
const posts = await fetchPosts()
const comments = await fetchComments()
return (
<div>
<UserProfile user={user} />
<Posts posts={posts} />
<Comments comments={comments} />
</div>
)
}
GOOD - Stream components independently:
import { Suspense } from 'react'
export default function Dashboard() {
return (
<div>
<Suspense fallback={<UserSkeleton />}>
<UserProfile />
</Suspense>
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
)
}
async function UserProfile() {
const user = await fetchUser()
return <div>{user.name}</div>
}
Server Actions
BAD - Client-side fetch to API route:
'use client'
export default function CreatePostForm() {
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
await fetch('/api/posts', { method: 'POST', body: JSON.stringify({}) })
}
return <form onSubmit={handleSubmit}>...</form>
}
GOOD - Server Actions with progressive enhancement:
// app/posts/actions.ts
'use server'
import { revalidateTag, updateTag } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.posts.create({ data: { title } })
revalidateTag('posts', 'max') // SWR with cacheLife profile (required in 16)
return { success: true }
}
// For read-your-writes (user sees changes instantly):
export async function updatePost(id: string, formData: FormData) {
await db.posts.update({ where: { id }, data: { title: formData.get('title') } })
updateTag(`post-${id}`) // New in 16: immediate cache expiry
}
GOOD - Server Actions with useActionState:
'use client'
import { useActionState } from 'react'
import { updateProfile } from './actions'
export default function ProfileForm() {
const [state, formAction, isPending] = useActionState(updateProfile, null)
return (
<form action={formAction}>
<input name="name" />
{state?.error && <p className="error">{state.error}</p>}
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Update'}
</button>
</form>
)
}
Caching APIs (Updated in 16)
BAD - Old revalidateTag with single argument (deprecated):
revalidateTag('posts') // Deprecated single-arg form
GOOD - New caching APIs:
import { revalidateTag, updateTag, refresh } from 'next/cache'
// SWR revalidation with cacheLife profile
revalidateTag('posts', 'max') // Background revalidation, serve stale
revalidateTag('news', 'hours') // Revalidate within hours profile
revalidateTag('data', { expire: 3600 }) // Custom TTL
// Read-your-writes (Server Actions only)
updateTag('user-profile') // Expire + immediate fresh read
// Refresh uncached data (Server Actions only)
refresh() // Refresh dynamic data without touching cache
React 19.2 Features
View Transitions:
'use client'
import { ViewTransition } from 'react'
export default function PhotoGrid({ photos }) {
return (
<div className="grid">
{photos.map(photo => (
<ViewTransition key={photo.id} name={`photo-${photo.id}`}>
<Link href={`/photos/${photo.id}`}>
<img src={photo.url} alt={photo.title} />
</Link>
</ViewTransition>
))}
</div>
)
}
Activity (background rendering):
'use client'
import { Activity } from 'react'
export default function TabPanel({ activeTab, tabs }) {
return (
<div>
{tabs.map(tab => (
<Activity key={tab.id} mode={tab.id === activeTab ? 'visible' : 'hidden'}>
<TabContent tab={tab} />
</Activity>
))}
</div>
)
}
Error Boundaries
// app/posts/[id]/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Something went wrong!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Try again</button>
</div>
)
}
// app/posts/[id]/loading.tsx
export default function Loading() {
return <div className="skeleton">Loading post...</div>
}
// app/posts/[id]/not-found.tsx
export default function NotFound() {
return <div>Post not found</div>
}
Parallel Routes
BAD - Conditional rendering in one component:
export default function Dashboard({ searchParams }) {
return (
<div>
<Sidebar />
{searchParams.modal === 'settings' && <SettingsModal />}
</div>
)
}
GOOD - Parallel routes with slots (all slots need default.tsx in 16):
app/dashboard/
├── @modal/
│ ├── settings/page.tsx
│ ├── profile/page.tsx
│ └── default.tsx # REQUIRED in 16
├── @sidebar/
│ ├── page.tsx
│ └── default.tsx # REQUIRED in 16
├── layout.tsx
└── page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children, modal, sidebar,
}: {
children: React.ReactNode
modal: React.ReactNode
sidebar: React.ReactNode
}) {
return (
<div className="dashboard">
<aside>{sidebar}</aside>
<main>{children}</main>
{modal}
</div>
)
}
// app/dashboard/@modal/default.tsx - REQUIRED
import { notFound } from 'next/navigation'
export default function Default() {
return null // or notFound()
}
Route Handlers
// app/api/posts/route.ts
import { NextRequest } from 'next/server'
import { z } from 'zod'
const postSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
})
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const validated = postSchema.parse(body)
const post = await db.posts.create({ data: validated })
return Response.json({ post }, { status: 201 })
} catch (error) {
if (error instanceof z.ZodError) {
return Response.json({ error: error.errors }, { status: 400 })
}
return Response.json({ error: 'Internal error' }, { status: 500 })
}
}
// Async params in route handlers (required in 16)
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params
const post = await db.posts.findUnique({ where: { id } })
if (!post) return Response.json({ error: 'Not found' }, { status: 404 })
return Response.json({ post })
}
Procedural Workflow: New Feature
- Create route segment in
app/withpage.tsx - Add layout.tsx if shared UI needed across child routes
- Use Server Component by default, fetch data directly
- Add
"use cache"directive for pages/components that benefit from caching - Add Suspense boundaries for slow data fetching
- Create loading.tsx and error.tsx for each route
- Add Server Actions in
actions.tsfor mutations (useupdateTagfor read-your-writes) - Add proxy.ts if auth/redirects needed (not middleware.ts)
- Add default.tsx to all parallel route slots
- Enable Turbopack FS caching for large projects (
turbopackFileSystemCacheForDev)
Turbopack Configuration
// next.config.ts
const nextConfig = {
// Turbopack is default in 16, no config needed
// To use webpack instead:
// webpack: true, // or run: next dev --webpack
// Enable filesystem caching (beta) for faster restarts
experimental: {
turbopackFileSystemCacheForDev: true,
},
// React Compiler (stable in 16, opt-in)
reactCompiler: true,
// Cache Components
cacheComponents: true,
}
export default nextConfig
Use proxy.ts instead of middleware.ts. All params are async. All parallel slots need default.tsx. Use "use cache" instead of experimental.ppr. Turbopack is default.