nextjs-development
Next.js Development
Comprehensive reference for Next.js (latest: 16.1.6) with the App Router, TypeScript, and modern patterns. Covers project structure, Server/Client Components, data fetching, caching with the use cache directive, Server Actions, MCP devtools integration, and performance optimization.
Skill Paths
- Workspace:
.github/skills/nextjs-development/SKILL.md - Global:
C:/Users/LOQ/.agents/skills/nextjs-development/SKILL.md
Activation Conditions
App Router & Routing
- Creating or modifying
page.tsx,layout.tsx,loading.tsx,error.tsx,not-found.tsx - Working with dynamic routes
[slug], catch-all[...slug], optional catch-all[[...slug]] - Parallel routes
@slot, intercepting routes, route groups(group) - New v15/v16 file conventions:
forbidden.tsx,proxy.ts,template.tsx,unauthorized.tsx
Server & Client Components
- Deciding when to use
"use client"or"use server"directives - Component boundary questions, RSC + RCC composition patterns
- Passing Server Components as children/props to Client Components
taintAPI for data security
Data Fetching & Caching
- Using
use cachedirective (replacescache: 'force-cache') cacheTag(),cacheLife(),revalidateTag(),revalidatePath()- Async Request APIs:
await cookies(),await headers(),await params,await searchParams after()for post-response work,connection()for dynamic rendering
Server Actions & Forms
"use server"in functions or module scope<Form>component with client-side navigation- Form validation, optimistic updates, error handling
after()for side-effects after action completes
Performance & Turbopack
next devwith Turbopack (default in v15+, stable)- Image optimization with
next/image - Font subsetting with
next/font - Lazy loading, bundle optimization,
serverComponentsHmrCache
Next.js MCP Dev Tools
- Querying live errors, logs, routes from the running dev server
- Using
next-devtools-mcpwith coding agents (requires Next.js 16+) - Upgrading to Next.js 16 with codemods
- Enabling Cache Components feature
Part 1: Project Setup & Config
Creating a New Project
npx create-next-app@latest my-app --typescript --tailwind --eslint --app
TypeScript Config (next.config.ts)
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
reactCompiler: true, // stable in v16
reactStrictMode: true,
serverExternalPackages: ['sharp'], // renamed from serverComponentsExternalPackages in v15
experimental: {
turbopackFileSystemCache: true, // persist Turbopack cache across restarts
serverComponentsHmrCache: true, // cache fetch responses during HMR
},
cacheLife: { // custom cache profiles
frequent: { stale: 60, revalidate: 60, expire: 3600 },
daily: { stale: 3600, revalidate: 3600, expire: 86400 },
},
}
export default nextConfig
Project Structure
my-app/
├── app/
│ ├── layout.tsx # Root layout (required)
│ ├── page.tsx # Home page
│ ├── loading.tsx # Streaming skeleton
│ ├── error.tsx # Error boundary
│ ├── not-found.tsx # 404 page
│ ├── forbidden.tsx # 403 page (v16)
│ ├── unauthorized.tsx # 401 page (v16)
│ ├── (marketing)/ # Route group (no URL segment)
│ │ └── about/page.tsx
│ ├── blog/
│ │ └── [slug]/page.tsx # Dynamic route
│ └── api/
│ └── route.ts # Route Handler
├── components/ # Shared RSC/RCC components
├── lib/ # Server utilities
├── public/ # Static assets
├── next.config.ts # TypeScript config (v15+)
├── .mcp.json # MCP server config (v16)
└── instrumentation.ts # Server lifecycle hooks (stable v15)
Part 2: App Router Routing
File Conventions
| File | Purpose |
|---|---|
page.tsx |
UI for the route segment, makes it publicly accessible |
layout.tsx |
Shared UI that persists across navigations |
template.tsx |
Like layout, but remounts on navigation |
loading.tsx |
Suspense skeleton; shown while page loads |
error.tsx |
Isolate errors; "use client" required |
not-found.tsx |
Rendered by notFound() or 404 |
forbidden.tsx |
Rendered by forbidden() (v16) |
unauthorized.tsx |
Rendered by unauthorized() (v16) |
route.ts |
API endpoint (cannot coexist with page.tsx at same level) |
proxy.ts |
Lightweight HTTP proxy (v16) |
middleware.ts |
Runs before request completes (project root) |
instrumentation.ts |
Server lifecycle, OpenTelemetry (stable v15) |
instrumentation-client.ts |
Client-side performance monitoring (v16) |
Dynamic Routes
// app/blog/[slug]/page.tsx
// IMPORTANT: params is now async in Next.js 15+
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params // must await in v15+
return <h1>{slug}</h1>
}
// Generate static paths
export async function generateStaticParams() {
const posts = await fetchPosts()
return posts.map((post) => ({ slug: post.slug }))
}
Route Groups & Parallel Routes
app/
├── (auth)/ # Route group: no URL impact
│ ├── login/page.tsx # /login
│ └── register/page.tsx # /register
├── @modal/ # Parallel route (slot)
│ └── photo/[id]/page.tsx
├── layout.tsx # Receives { children, modal } props
└── page.tsx
Intercepting Routes
app/
├── photos/[id]/page.tsx # Full page: /photos/123
└── @modal/
└── (.)photos/[id]/ # Intercept same-level route
└── page.tsx # Renders as modal without full navigation
searchParams (async in v15+)
// app/search/page.tsx
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<{ q: string; page: string }>
}) {
const { q, page } = await searchParams // must await in v15+
return <Results query={q} page={Number(page)} />
}
Part 3: Server & Client Components
Decision Tree
Does the component need:
- onClick, onChange, event handlers? → "use client"
- useState, useEffect, useReducer? → "use client"
- Browser-only APIs (window, localStorage)? → "use client"
- useRouter, useParams, useSearchParams? → "use client"
Otherwise:
- Direct DB/API access without extra fetch? → Server Component (default)
- Large dependencies (marked-js, date-fns)? → Server Component (not in JS bundle)
- Access cookies(), headers(), auth tokens? → Server Component
Component Composition Pattern
// ✅ Pass Server Components as children to Client Components
// app/page.tsx (Server Component)
import { ClientWrapper } from '@/components/ClientWrapper'
import { ServerData } from '@/components/ServerData'
export default function Page() {
return (
<ClientWrapper>
<ServerData /> {/* Server Component as child — no "use client" boundary issue */}
</ClientWrapper>
)
}
// components/ClientWrapper.tsx
"use client"
import { useState } from 'react'
export function ClientWrapper({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return <div onClick={() => setOpen(!open)}>{children}</div>
}
Directives Reference
| Directive | Where | Effect |
|---|---|---|
"use client" |
Top of file | All exports are Client Components |
"use server" |
Top of file or function | Marks Server Actions; top-of-file applies to all exports |
"use cache" |
Top of file or function | Marks a component/function as a Cache Component |
"use cache: private" |
Top of file or function | Cache Component, private (user-specific) data |
"use cache: remote" |
Top of file or function | Cache Component, persisted remotely |
Data Security with taint
// next.config.ts
const nextConfig: NextConfig = {
experimental: { taint: true }
}
// lib/user.ts (Server)
import { experimental_taintUniqueValue } from 'react'
export async function getUser(id: string) {
const user = await db.user.findUnique({ where: { id } })
// Prevent accidental serialization of sensitive fields
experimental_taintUniqueValue(
'Do not pass user.passwordHash to Client',
user,
user.passwordHash
)
return user
}
Part 4: Data Fetching & Caching
The use cache Directive (Next.js 15+)
use cache replaces the old cache: 'force-cache' approach and works at the file, component, or function level.
// Cache an entire async function
async function getProducts() {
'use cache'
const data = await fetch('https://api.example.com/products')
return data.json()
}
// Cache a Server Component
async function ProductList() {
'use cache'
cacheLife('daily') // use named profile from next.config.ts
cacheTag('products') // tag for targeted revalidation
const products = await getProducts()
return <ul>{products.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
cacheLife Profiles
// Built-in profiles
cacheLife('seconds') // stale: 0, revalidate: 1, expire: 60
cacheLife('minutes') // stale: 60, revalidate: 60, expire: 3600
cacheLife('hours') // stale: 3600, revalidate: 3600, expire: 86400
cacheLife('days') // stale: 86400, revalidate: 86400, expire: 604800
cacheLife('weeks') // stale: 604800, revalidate: 604800, expire: 2592000
cacheLife('max') // stale: 2592000, revalidate: 2592000, expire: Infinity
// Custom profile (defined in next.config.ts)
cacheLife('frequent') // stale: 60, revalidate: 60, expire: 3600
Targeted Revalidation with cacheTag
// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'
export async function updateProduct(id: string, data: FormData) {
await db.products.update({ where: { id }, data: Object.fromEntries(data) })
revalidateTag('products') // invalidates all cached items with this tag
revalidateTag(`product-${id}`) // fine-grained invalidation
}
// app/products/[id]/page.tsx
async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
'use cache'
const { id } = await params
cacheTag('products', `product-${id}`)
const product = await db.products.findUnique({ where: { id } })
return <Product data={product} />
}
fetch Cache Behavior (v15+ defaults changed)
// GET route handlers are NO LONGER cached by default in v15+
// Explicitly opt-in to caching:
const res = await fetch('https://api.example.com/data', {
next: { revalidate: 3600, tags: ['products'] }
})
// Force dynamic (never cache):
const res = await fetch('https://api.example.com/data', {
cache: 'no-store'
})
// ISR — revalidate every N seconds:
export const revalidate = 3600 // segment-level option
Async Request APIs (v15 Breaking Change)
import { cookies, headers } from 'next/headers'
// BEFORE (v14): synchronous
const cookieStore = cookies()
// AFTER (v15+): must await
const cookieStore = await cookies()
const headersList = await headers()
// params and searchParams also async in page/layout props
const { slug } = await params
const { q } = await searchParams
after() — Post-Response Side Effects
import { after } from 'next/server'
export async function GET(request: Request) {
const data = await fetchData()
// Fires AFTER response is sent to client
after(async () => {
await logAnalyticsEvent('data-fetched', { timestamp: Date.now() })
})
return Response.json(data)
}
connection() — Force Dynamic Rendering
import { connection } from 'next/server'
export default async function Page() {
// Signals this component requires a live request (opts out of static rendering)
await connection()
const realTimeData = await fetchLiveData()
return <Dashboard data={realTimeData} />
}
Part 5: Server Actions & Forms
Server Actions
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(10),
})
export async function createPost(formData: FormData) {
const parsed = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
})
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors }
}
const post = await db.posts.create({ data: parsed.data })
revalidatePath('/blog')
redirect(`/blog/${post.id}`)
}
<Form> Component (Next.js 15+)
import Form from 'next/form'
export default function SearchForm() {
// <Form> replaces <form> for client-side navigation + prefetching
return (
<Form action="/search">
<input name="q" placeholder="Search..." />
<button type="submit">Search</button>
</Form>
)
}
Optimistic Updates with useOptimistic
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleLike } from '@/app/actions'
export function LikeButton({ postId, initialLikes }: Props) {
const [optimisticLikes, addOptimisticLike] = useOptimistic(
initialLikes,
(state, delta: number) => state + delta
)
const [isPending, startTransition] = useTransition()
return (
<button
onClick={() => startTransition(async () => {
addOptimisticLike(1)
await toggleLike(postId)
})}
disabled={isPending}
>
{optimisticLikes} Likes
</button>
)
}
Part 6: Next.js MCP Dev Tools
The next-devtools-mcp package enables coding agents to connect to the live Next.js development server. Requires Next.js 16+.
Setup
// .mcp.json (project root)
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
Next.js 16+ includes a built-in MCP endpoint at /_next/mcp in the development server. next-devtools-mcp automatically discovers and connects to running instances — even across multiple ports.
Available MCP Tools
| Tool | What It Does |
|---|---|
get_errors |
Retrieve current build, runtime, and type errors from the dev server |
get_logs |
Get the path to the dev log file (browser console + server output) |
get_page_metadata |
Get metadata about specific pages: routes, components, rendering type |
get_project_metadata |
Retrieve project structure, next.config, and dev server URL |
get_server_action_by_id |
Look up Server Actions by ID to find source file and function name |
nextjs_docs |
Query comprehensive Next.js documentation and best practices |
nextjs_runtime |
Interact with the running Next.js instance |
upgrade_nextjs_16 |
Automated upgrade guide to Next.js 16 with codemods |
enable_cache_components |
Setup and configuration guide for Cache Components |
Usage Patterns
# Ask the agent about runtime state
"What errors are currently in my application?"
→ Agent calls get_errors → analyzes build/type/runtime errors → suggests fixes
# Debug a specific route
"Why is /dashboard rendering statically instead of dynamically?"
→ Agent calls get_page_metadata with route=/dashboard → shows rendering config
# Navigate the codebase
"What Server Actions exist in this app?"
→ Agent calls get_project_metadata → then get_server_action_by_id for each action
# Upgrade workflow
"Help me upgrade to Next.js 16"
→ Agent calls upgrade_nextjs_16 → runs codemods → handles breaking changes
# Enable new features
"Set up Cache Components for this project"
→ Agent calls enable_cache_components → configures next.config.ts + shows patterns
Part 7: Performance & Turbopack
Turbopack (Default in v15+)
# Turbopack is now the default — no flag needed
npm run dev # Uses Turbopack automatically
# Opt back to webpack if needed
npm run dev -- --webpack
# Enable Turbopack filesystem cache (persist across restarts)
# next.config.ts
experimental: { turbopackFileSystemCache: true }
Benchmark vs webpack: 76.7% faster cold starts, 96.3% faster HMR.
Image Optimization
import Image from 'next/image'
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // LCP image: preloads synchronously
sizes="(max-width: 768px) 100vw, 1200px"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,..."
/>
)
}
Font Optimization
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter', // CSS variable for Tailwind
display: 'swap',
})
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.variable}>
<body>{children}</body>
</html>
)
}
React Compiler
// next.config.ts — stable in v16
const nextConfig: NextConfig = {
reactCompiler: true, // eliminates manual useMemo/useCallback
}
Bundle Optimization
// next.config.ts
const nextConfig: NextConfig = {
// Avoid importing entire icon libraries
// next/font handles subsetting automatically
bundlePagesRouterDependencies: true, // renamed from bundlePagesExternals in v15
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react'],
},
}
Part 8: Metadata & SEO
Static Metadata
// app/layout.tsx or app/page.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | My App',
default: 'My App',
},
description: 'App description for SEO',
openGraph: {
type: 'website',
url: 'https://example.com',
images: [{ url: '/og-image.jpg', width: 1200, height: 630 }],
},
robots: { index: true, follow: true },
metadataBase: new URL('https://example.com'),
}
Dynamic Metadata
// app/blog/[slug]/page.tsx
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params // async in v15+
const post = await getPost(slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
images: [{ url: post.cover, width: 1200, height: 630 }],
},
}
}
Part 9: Route Handlers & Middleware
Route Handlers (Uncached by Default in v15+)
// app/api/products/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { after } from 'next/server'
// GET is NO LONGER cached by default in v15+
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const category = searchParams.get('category')
const products = await db.products.findMany({
where: category ? { category } : undefined,
})
// Side effect after response
after(() => logRequest(request.url))
return NextResponse.json(products)
}
// Opt-in to caching for a route segment
export const revalidate = 3600 // revalidate every hour
export const dynamic = 'force-static' // always static
Middleware
// middleware.ts (project root)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const token = request.cookies.get('auth-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*', '/api/protected/:path*'],
}
Part 10: Instrumentation
Server-Side Lifecycle (instrumentation.ts)
// instrumentation.ts (stable in v15)
export async function register() {
// Runs once when the server starts (Node.js + Edge)
if (process.env.NEXT_RUNTIME === 'nodejs') {
// Initialize OpenTelemetry, Sentry, etc.
const { initTracing } = await import('./lib/tracing')
await initTracing()
}
}
export async function onRequestError(
error: Error,
request: { path: string; method: string },
context: { routeType: string }
) {
// Centralized error reporting
await reportError(error, { path: request.path })
}
Client-Side Instrumentation (instrumentation-client.ts)
// instrumentation-client.ts (v16)
export function onRouteChange({ path }: { path: string }) {
// Track route changes for analytics
analytics.track('page_view', { path })
}
export function onCaughtError(error: Error) {
// Capture client-side errors
Sentry.captureException(error)
}
Part 11: Auth Interrupts (v16)
// middleware.ts or Server Component
import { forbidden, unauthorized } from 'next/navigation'
export default async function AdminPage() {
const session = await getSession()
if (!session) {
unauthorized() // renders unauthorized.tsx
}
if (!session.user.isAdmin) {
forbidden() // renders forbidden.tsx
}
return <AdminDashboard />
}
Part 12: Upgrading to v15/v16
Automated Codemods
# Upgrade to v15 (handles async Request APIs automatically)
npx @next/codemod@latest upgrade
# Or upgrade to v16 specifically
npx @next/codemod@latest upgrade next@16
# Available codemods
npx @next/codemod@latest next-async-request-api .
npx @next/codemod@latest next-og-import .
Key v15 Breaking Changes
| Change | Before (v14) | After (v15+) |
|---|---|---|
cookies() |
sync | await cookies() |
headers() |
sync | await headers() |
params |
sync | await params |
searchParams |
sync | await searchParams |
| GET Route Handlers | cached by default | not cached by default |
| Client Router Cache | cached segments | not cached by default |
serverComponentsExternalPackages |
old name | serverExternalPackages |
bundlePagesExternals |
old name | bundlePagesRouterDependencies |
Related Skills
| Skill | Use When |
|---|---|
react-development |
React 19 hooks, component patterns, performance (useMemo, useCallback, Suspense) |
vite-development |
Non-Next.js React apps, Vite build config, plugin setup |
javascript-development |
TypeScript/JS patterns, async/await, fetch, error handling |
web-testing |
Playwright E2E tests, Vitest unit tests for Next.js apps |
devops-tooling |
GitHub Actions CI/CD for Next.js, git workflow |
azure-integrations |
Deploying Next.js to Azure Static Web Apps or App Service |