Ai nextjs

Next.js App Router best practices — Server Components, data fetching, caching, routing, middleware, metadata, error handling, streaming, Server Actions, and performance optimization for Next.js 14-16+.

install
source · Clone the upstream repo
git clone https://github.com/wpank/ai
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/wpank/ai "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/frontend/nextjs" ~/.claude/skills/wpank-ai-nextjs && rm -rf "$T"
manifest: skills/frontend/nextjs/SKILL.md
source content

Next.js App Router

Apply these patterns when building, reviewing, or debugging Next.js App Router applications.

Installation

OpenClaw / Moltbot / Clawbot

npx clawhub@latest install nextjs

WHEN

  • Building Next.js applications with App Router
  • Migrating from Pages Router to App Router
  • Implementing Server Components and streaming
  • Setting up parallel and intercepting routes
  • Optimizing data fetching and caching
  • Building full-stack features with Server Actions
  • Debugging hydration errors or RSC boundary issues

Rendering Modes

ModeWhereWhen to Use
Server ComponentsServer onlyData fetching, secrets, heavy computation
Client ComponentsBrowserInteractivity, hooks, browser APIs
Static (SSG)Build timeContent that rarely changes
Dynamic (SSR)Request timePersonalized or real-time data
StreamingProgressiveLarge pages, slow data sources

Server vs Client Decision Tree

Does it need...?
├── useState, useEffect, event handlers, browser APIs
│   └── Client Component ('use client')
├── Direct data fetching, no interactivity
│   └── Server Component (default)
└── Both?
    └── Split: Server parent fetches data → Client child handles UI

File Conventions

See file-conventions.md for complete reference.

app/
├── layout.tsx          # Shared UI wrapper (persists across navigations)
├── page.tsx            # Route UI
├── loading.tsx         # Suspense fallback (automatic)
├── error.tsx           # Error boundary (must be 'use client')
├── not-found.tsx       # 404 UI
├── route.ts            # API endpoint (cannot coexist with page.tsx)
├── template.tsx        # Like layout but re-mounts on navigation
├── default.tsx         # Parallel route fallback
└── opengraph-image.tsx # OG image generation

Route segments:

[slug]
dynamic,
[...slug]
catch-all,
[[...slug]]
optional catch-all,
(group)
route group,
@slot
parallel route,
_folder
private (excluded from routing).

Data Fetching Patterns

Choose the right pattern for each use case. See data-patterns.md for full decision tree.

PatternUse CaseCaching
Server Component fetchInternal reads (preferred)Full Next.js caching
Server ActionMutations, form submissionsPOST only, no cache
Route HandlerExternal APIs, webhooks, public RESTGET can be cached
Client fetch → APIClient-side reads (last resort)HTTP cache headers

Server Component Data Fetching (Preferred)

// app/products/page.tsx — Server Component by default
export default async function ProductsPage() {
  const products = await db.product.findMany() // Direct DB access, no API layer
  return <ProductGrid products={products} />
}

Avoiding Data Waterfalls

// BAD: Sequential — each awaits before the next starts
const user = await getUser()
const posts = await getPosts()

// GOOD: Parallel fetching
const [user, posts] = await Promise.all([getUser(), getPosts()])

// GOOD: Streaming with Suspense — each section loads independently
<Suspense fallback={<UserSkeleton />}><UserSection /></Suspense>
<Suspense fallback={<PostsSkeleton />}><PostsSection /></Suspense>

Server Actions (Mutations)

// app/actions.ts
'use server'
import { revalidateTag } from 'next/cache'

export async function addToCart(productId: string) {
  const cookieStore = await cookies()
  const sessionId = cookieStore.get('session')?.value
  if (!sessionId) redirect('/login')

  await db.cart.upsert({
    where: { sessionId_productId: { sessionId, productId } },
    update: { quantity: { increment: 1 } },
    create: { sessionId, productId, quantity: 1 },
  })
  revalidateTag('cart')
  return { success: true }
}

Caching Strategy

MethodSyntaxUse Case
No cache
fetch(url, { cache: 'no-store' })
Always-fresh data
Static
fetch(url, { cache: 'force-cache' })
Rarely changes
ISR
fetch(url, { next: { revalidate: 60 } })
Time-based refresh
Tag-based
fetch(url, { next: { tags: ['products'] } })
On-demand invalidation

Invalidate from Server Actions:

'use server'
import { revalidateTag, revalidatePath } from 'next/cache'

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data })
  revalidateTag('products')   // Invalidate by tag
  revalidatePath('/products') // Invalidate by path
}

RSC Boundaries

Props crossing Server → Client boundary must be JSON-serializable. See rsc-boundaries.md.

Prop TypeValid?Fix
string
,
number
,
boolean
Yes
Plain object / arrayYes
Server Action (
'use server'
)
Yes
Function
() => {}
NoDefine inside client component
Date
object
NoUse
.toISOString()
Map
,
Set
, class instance
NoConvert to plain object/array

Critical rule: Client Components cannot be

async
. Fetch data in a Server Component parent and pass it down.

Async APIs (Next.js 15+)

params
,
searchParams
,
cookies()
, and
headers()
are all async. See async-patterns.md.

// Pages and layouts — always await params
type Props = { params: Promise<{ slug: string }> }

export default async function Page({ params }: Props) {
  const { slug } = await params
}

// Server functions
const cookieStore = await cookies()
const headersList = await headers()

// Non-async components — use React.use()
import { use } from 'react'
export default function Page({ params }: Props) {
  const { slug } = use(params)
}

Routing Patterns

Route Organization

PatternSyntaxPurpose
Route groups
(marketing)/
Organize without affecting URL
Parallel routes
@analytics/
Multiple independent sections in one layout
Intercepting routes
(.)photos/[id]
Modal overlays on soft navigation
Private folders
_components/
Exclude from routing

Parallel Routes & Modals

See parallel-routes.md for complete modal pattern.

Key rules:

  • Every
    @slot
    folder must have a
    default.tsx
    (returns
    null
    ) or you get 404 on refresh
  • Close modals with
    router.back()
    , never
    router.push()
    or
    <Link>
  • Intercepting route matchers:
    (.)
    same level,
    (..)
    one level up,
    (...)
    from root

Metadata & SEO

See metadata.md for OG images, sitemaps, and file conventions.

// Static metadata (layout or page)
export const metadata: Metadata = {
  title: { default: 'My App', template: '%s | My App' },
  description: 'Built with Next.js',
}

// Dynamic metadata
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)
  return {
    title: post.title,
    description: post.description,
    openGraph: { images: [{ url: post.image, width: 1200, height: 630 }] },
  }
}

Metadata is Server Components only. If a page has

'use client'
, extract metadata to a parent layout.

Error Handling

See error-handling.md for full patterns including auth errors.

// app/blog/error.tsx — must be 'use client'
'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>
  )
}

Critical gotcha:

redirect()
,
notFound()
,
forbidden()
, and
unauthorized()
throw special errors. Never catch them in try/catch:

// BAD: redirect throw is caught — navigation fails!
try {
  await db.post.create({ data })
  redirect(`/posts/${post.id}`)
} catch (error) {
  return { error: 'Failed' } // Catches the redirect too!
}

// GOOD: Call redirect outside try-catch
let post
try { post = await db.post.create({ data }) }
catch (error) { return { error: 'Failed' } }
redirect(`/posts/${post.id}`)

Streaming with Suspense

export default async function ProductPage({ params }: Props) {
  const { id } = await params
  const product = await getProduct(id) // Blocking — loads first

  return (
    <div>
      <ProductHeader product={product} />
      <Suspense fallback={<ReviewsSkeleton />}>
        <Reviews productId={id} />       {/* Streams in independently */}
      </Suspense>
      <Suspense fallback={<RecommendationsSkeleton />}>
        <Recommendations productId={id} /> {/* Streams in independently */}
      </Suspense>
    </div>
  )
}

Hooks That Require Suspense Boundaries

HookSuspense Required
useSearchParams()
Always (or entire page becomes CSR)
usePathname()
In dynamic routes
useParams()
No
useRouter()
No

Performance

  • Always use
    next/image
    over
    <img>
    — see image-optimization.md
  • Always use
    next/link
    over
    <a>
    — client-side navigation with prefetching
  • Always use
    next/font
    — see font-optimization.md
  • Always use
    next/script
    — see scripts.md
  • Set
    priority
    on above-the-fold images (LCP)
  • Add
    sizes
    when using
    fill
    — without it, the largest image variant downloads
  • Dynamic imports for heavy client components:
    const Chart = dynamic(() => import('./Chart'))
  • Use
    generateStaticParams
    to pre-render dynamic routes at build time

Route Handlers

See route-handlers.md for API endpoint patterns.

Bundling

See bundling.md for fixing third-party package issues, server-incompatible packages, and ESM/CommonJS problems.

Hydration Errors

See hydration-errors.md for all causes and fixes.

CauseFix
Browser APIs (
window
,
localStorage
)
Client component with
useEffect
mount check
new Date().toLocaleString()
Render on client with
useEffect
Math.random()
for IDs
Use
useId()
hook
<p><div>...</div></p>
Fix invalid HTML nesting
Third-party scripts modifying DOMUse
next/script
with
afterInteractive

Self-Hosting

See self-hosting.md for Docker, PM2, cache handlers, and deployment checklist.

Key points:

  • Use
    output: 'standalone'
    for Docker — creates minimal production bundle
  • Copy
    public/
    and
    .next/static/
    separately (not included in standalone)
  • Set
    HOSTNAME="0.0.0.0"
    for containers
  • Multi-instance ISR requires a custom cache handler (Redis/S3) — filesystem cache breaks
  • Set health check endpoint at
    /api/health

NEVER Do

NeverWhyInstead
Add
'use client'
by default
Bloats client bundle, loses Server Component benefitsServer Components are default — add
'use client'
only for interactivity
Make client components
async
Not supported — will crashFetch in Server Component parent, pass data as props
Pass
Date
/
Map
/functions to client
Not serializable across RSC boundarySerialize to string/plain object, or use Server Actions
Fetch from own API in Server ComponentsUnnecessary round-trip — you're already on the serverAccess DB/service directly
Wrap
redirect()
/
notFound()
in try-catch
They throw special errors that get swallowedCall outside try-catch or use
unstable_rethrow()
Skip
loading.tsx
or Suspense fallbacks
Users see blank page during data loadingAlways provide loading states
Use
useSearchParams
without Suspense
Entire page silently falls back to CSRWrap in
<Suspense>
boundary
Use
router.push()
to close modals
Breaks history, modal can flash/persistUse
router.back()
Use
@vercel/og
for OG images
Built into Next.js alreadyImport from
next/og
Omit
default.tsx
in parallel route slots
Hard navigation (refresh) returns 404Add
default.tsx
returning
null
Use Edge runtime unless requiredLimited APIs, most npm packages breakDefault Node.js runtime covers 95% of cases
Skip
sizes
prop on
fill
images
Downloads largest image variant alwaysAdd
sizes="100vw"
or appropriate breakpoints
Import fonts in multiple componentsCreates duplicate instancesImport once in layout, use CSS variable
Use
<link>
for Google Fonts
No optimization, blocks renderingUse
next/font

Reference Files

FileTopic
rsc-boundaries.mdServer/Client boundary rules, serialization
data-patterns.mdFetching decision tree, waterfall avoidance
error-handling.mdError boundaries, redirect gotcha, auth errors
async-patterns.mdNext.js 15+ async params/cookies/headers
metadata.mdSEO, OG images, sitemaps, file conventions
parallel-routes.mdModal pattern, intercepting routes, gotchas
hydration-errors.mdCauses, debugging, fixes
self-hosting.mdDocker, PM2, cache handlers, deployment
file-conventions.mdProject structure, special files, middleware
bundling.mdThird-party packages, SSR issues, Turbopack
image-optimization.mdnext/image best practices
font-optimization.mdnext/font best practices
scripts.mdnext/script, third-party loading
route-handlers.mdAPI endpoints, request/response helpers