# Nextjs Patterns

> When to activate: Next.js 14/15, App Router, Server Components, Route Handlers, metadata, caching, middleware, ISR

- Skill: `mattakushi432/nextjs-patterns` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/nextjs-patterns`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/nextjs-patterns/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/nextjs-patterns

---


# Next.js Patterns

## App Router Structure
```
app/
├── layout.tsx          # Root layout (Server Component)
├── page.tsx            # Home route
├── loading.tsx         # Suspense boundary
├── error.tsx           # Error boundary ('use client')
├── not-found.tsx       # 404 UI
├── (marketing)/        # Route group — no URL segment
│   ├── about/page.tsx
│   └── blog/[slug]/page.tsx
├── (app)/              # Auth-protected group
│   └── dashboard/
│       ├── layout.tsx
│       └── page.tsx
└── api/
    └── webhooks/route.ts
```

## Server vs Client Components
```tsx
// Server Component (default) — async/await, direct DB access
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await db.product.findUnique({ where: { id: params.id } })
  return <ProductDetail product={product} />
}

// Client Component
'use client'
function AddToCart({ productId }: { productId: string }) {
  const [loading, setLoading] = useState(false)
  // event handlers, browser APIs, hooks work here
}
```

## Data Fetching
```tsx
// ISR — revalidate every hour
const res = await fetch(url, { next: { revalidate: 3600 } })

// No cache (dynamic)
const res = await fetch(url, { cache: 'no-store' })

// Tag-based revalidation
const res = await fetch(url, { next: { tags: ['posts'] } })
// Trigger: revalidateTag('posts') from a Server Action
```

## Route Handlers
```ts
// app/api/users/route.ts
export async function GET(request: NextRequest) {
  const { searchParams } = new URL(request.url)
  const page = Number(searchParams.get('page') ?? '1')
  const users = await db.user.findMany({ skip: (page - 1) * 20, take: 20 })
  return NextResponse.json(users)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const user = await db.user.create({ data: body })
  return NextResponse.json(user, { status: 201 })
}

// Dynamic segment: app/api/users/[id]/route.ts
export async function GET(_req: NextRequest, { params }: { params: { id: string } }) {
  const user = await db.user.findUnique({ where: { id: params.id } })
  if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 })
  return NextResponse.json(user)
}
```

## Metadata API
```tsx
// Static
export const metadata: Metadata = {
  title: 'My App',
  description: 'Description',
  openGraph: { title: 'My App', images: ['/og.png'] },
}

// Dynamic
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
  const post = await getPost(params.slug)
  return { title: post.title, openGraph: { images: [post.image] } }
}
```

## Middleware
```ts
// middleware.ts
export function middleware(request: NextRequest) {
  const token = request.cookies.get('token')?.value
  if (request.nextUrl.pathname.startsWith('/dashboard') && !token) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  const response = NextResponse.next()
  response.headers.set('x-request-id', crypto.randomUUID())
  return response
}

export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
}
```

## generateStaticParams
```tsx
export async function generateStaticParams() {
  const posts = await getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}
```

## Loading UI & Streaming
```tsx
// app/dashboard/loading.tsx — auto-wraps page in Suspense
export default function Loading() { return <DashboardSkeleton /> }

// Manual Suspense for granular streaming
export default async function Page() {
  return (
    <div>
      <StaticHeader />
      <Suspense fallback={<Skeleton />}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}
```

## Image & Font Optimization
```tsx
import Image from 'next/image'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'], display: 'swap' })

<Image src="/hero.jpg" alt="Hero" width={1200} height={600} priority />
```

## Environment Variables
```
DATABASE_URL=postgres://...           # server-only
NEXT_PUBLIC_API_URL=https://api...    # exposed to client (NEXT_PUBLIC_ prefix)
```

