---
name: nextjs
description: "Next.js patterns for SSR, ISR, App Router, and full-stack React. Use when working with Next.js projects."
detect:
  ["next.config.js", "next.config.mjs", "next.config.ts", "app/layout.tsx"]
category: frameworks
tier: 1
---

# Next.js Patterns — DOMYH Awesome Code

> **Version**: Next.js 16+ (Oct 2025)
> **Runtime**: React meta-framework with App Router
> **Philosophy**: Server-first, Turbopack default, Cache Components

---

## 🎯 When to Use This Skill

Use for: React SSR/SSG apps, Server Components, App Router, API routes, Turbopack builds.
**NOT for**: Pure React SPA (→ react skill), Node.js backend only (→ nodejs skill).

---

## 🌳 Decision Tree

```
Task → What Next.js pattern?
  ├─ Rendering
  │   ├─ Static content → SSG (generateStaticParams)
  │   ├─ Dynamic per-request → SSR (no-store)
  │   ├─ Periodic refresh → ISR (revalidate: N)
  │   └─ Mixed static+dynamic → Cache Components ("use cache")
  ├─ Data
  │   ├─ Server-only → Server Component + direct DB
  │   ├─ Client interactive → TanStack Query + API route
  │   └─ Form mutations → Server Functions ("use server")
  ├─ Routing
  │   ├─ App Router (default) → app/ directory
  │   ├─ Route groups → (auth)/ for shared layout
  │   └─ API endpoints → app/api/route.ts
  └─ Auth
      ├─ Full-featured → NextAuth v5
      └─ Managed → Clerk
```

---

## 📦 Recommended Stack

> See `data/core.yaml` for full library comparison tables.

**Core**: Next.js 16 + React 19 + TypeScript 5.5+ + Turbopack (default)
**UI**: shadcn/ui + Tailwind CSS 4 · **State**: TanStack Query + Zustand
**Data**: Prisma/Drizzle · **Auth**: NextAuth v5/Clerk · **IDE**: VS Code + ESLint

---

## 🏗️ Next.js 16 New Features

### Turbopack (Default Bundler)

```bash
# ✅ Turbopack is now the DEFAULT bundler in Next.js 16
# 2-5x faster production builds
# 10x faster Fast Refresh
# No configuration needed

# Enable FS cache for even faster builds (beta)
# next.config.ts
export default {
  experimental: {
    turbopack: {
      fileSystemCache: true,
    }
  }
}
```

### Cache Components (Replaces PPR)

```tsx
// ✅ Next.js 16: Cache Components with 'use cache' directive
// PPR flag removed - use explicit caching instead

// app/products/page.tsx
import { unstable_cache } from "next/cache";

// Cache at component level
export default async function ProductsPage() {
  "use cache"; // ✅ New directive

  const products = await db.product.findMany();
  return <ProductList products={products} />;
}

// Enhanced cache APIs
import { revalidateTag, updateTag, refresh } from "next/cache";

export async function updateProduct(id: string, data: ProductData) {
  await db.product.update({ where: { id }, data });

  // ✅ New cache APIs in Next.js 16
  updateTag("products"); // Partial update
  refresh(); // Full refresh
  revalidateTag("products"); // Revalidate
}
```

### proxy.ts (Replaces middleware.ts)

```tsx
// ✅ Next.js 16: proxy.ts for network boundary control
// Provides clearer separation than middleware.ts

// proxy.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function proxy(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Auth check with clearer network boundary
  const token = request.cookies.get("token");
  if (pathname.startsWith("/dashboard") && !token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
```

### React 19.2 Integration

```tsx
// ✅ Next.js 16 bundles React 19.2
import { useOptimistic, useEffectEvent } from "react";

// View Transitions API
export function NavigationLink({ href, children }) {
  return (
    <Link href={href} viewTransition>
      {children}
    </Link>
  );
}

// useEffectEvent for stable event handlers
function ChatRoom({ roomId }) {
  const onMessage = useEffectEvent((msg) => {
    showNotification(msg);
  });

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.on("message", onMessage);
    return () => connection.disconnect();
  }, [roomId]); // onMessage not in deps!
}

// Activity component for offscreen rendering
import { Activity } from "react";

<Activity mode="hidden">
  <HeavyComponent />
</Activity>;
```

### React Compiler (Stable)

```tsx
// ✅ Next.js 16: Built-in React Compiler support
// Automatic memoization at build time

// next.config.ts
export default {
  experimental: {
    reactCompiler: true, // ✅ Stable in Next.js 16
  },
};

// No more manual useMemo/useCallback needed!
function ExpensiveComponent({ data }) {
  // Compiler auto-memoizes this
  const processed = data.map((item) => expensiveOperation(item));
  return <List items={processed} />;
}
```

### Async Request APIs (15+)

```tsx
// app/users/[id]/page.tsx
// ✅ params is now a Promise
export default async function UserPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const user = await fetchUser(id);
  return <UserProfile user={user} />;
}

// ✅ searchParams is also a Promise
export default async function SearchPage({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q } = await searchParams;
  const results = await search(q);
  return <SearchResults results={results} />;
}
```

---

## 📁 Project Structure

```
my-app/
├── app/
│   ├── layout.tsx          # Root layout
│   ├── page.tsx             # Home page
│   ├── loading.tsx          # Loading UI
│   ├── error.tsx            # Error boundary
│   ├── not-found.tsx        # 404 page
│   ├── (auth)/              # Route group (no URL segment)
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── dashboard/
│   │   ├── layout.tsx       # Nested layout
│   │   ├── page.tsx
│   │   └── settings/page.tsx
│   └── api/
│       └── users/route.ts   # API route
├── components/
│   ├── ui/                  # shadcn components
│   └── features/            # Feature components
├── lib/
│   ├── db.ts                # Database client
│   └── utils.ts             # Utilities
├── public/
├── next.config.ts
└── tailwind.config.ts
```

---

## 🔧 Server Components (Default)

### Data Fetching in Server Components

```tsx
// app/posts/page.tsx
// ✅ Server Component - default, no directive needed
import { db } from "@/lib/db";

export default async function PostsPage() {
  // Direct database access - no API needed
  const posts = await db.post.findMany({
    orderBy: { createdAt: "desc" },
    take: 10,
  });

  return (
    <div>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

// ✅ Static metadata
export const metadata = {
  title: "Posts",
  description: "All blog posts",
};

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

### Caching Strategies

```tsx
// ✅ Static (cached indefinitely)
const data = await fetch(url);

// ✅ Revalidate every 60 seconds (ISR)
const data = await fetch(url, {
  next: { revalidate: 60 },
});

// ✅ No cache (always fresh)
const data = await fetch(url, {
  cache: "no-store",
});

// ✅ Cached function with unstable_cache
import { unstable_cache } from "next/cache";

const getCachedUser = unstable_cache(
  async (id: string) => db.user.findUnique({ where: { id } }),
  ["user"],
  { revalidate: 3600, tags: ["users"] },
);

// ✅ Revalidate on demand
import { revalidatePath, revalidateTag } from "next/cache";

export async function updateUser(id: string, data: UserData) {
  await db.user.update({ where: { id }, data });
  revalidatePath(`/users/${id}`);
  revalidateTag("users");
}
```

---

## 🎨 Client Components

```tsx
"use client";

import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";

// ✅ Mark with "use client" for interactivity
export function UserForm({ userId }: { userId: string }) {
  const queryClient = useQueryClient();

  const { data: user, isLoading } = useQuery({
    queryKey: ["user", userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((r) => r.json()),
  });

  const mutation = useMutation({
    mutationFn: (data: FormData) =>
      fetch(`/api/users/${userId}`, {
        method: "PATCH",
        body: data,
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["user", userId] });
    },
  });

  if (isLoading) return <Skeleton />;

  return (
    <form action={mutation.mutate}>
      <input name="name" defaultValue={user.name} />
      <button type="submit" disabled={mutation.isPending}>
        {mutation.isPending ? "Saving..." : "Save"}
      </button>
    </form>
  );
}
```

---

## ⚡ Server Functions

```tsx
// app/actions.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { z } from "zod";

const UserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

export async function createUser(formData: FormData) {
  // ✅ Validate input
  const validated = UserSchema.parse({
    name: formData.get("name"),
    email: formData.get("email"),
  });

  // ✅ Database operation
  const user = await db.user.create({
    data: validated,
  });

  // ✅ Revalidate cache
  revalidatePath("/users");

  // ✅ Redirect
  redirect(`/users/${user.id}`);
}

// ✅ useOptimistic for instant UI feedback (React 19)
("use client");
import { useOptimistic } from "react";

export function LikeButton({ likes }: { likes: number }) {
  const [optimisticLikes, addOptimisticLike] = useOptimistic(
    likes,
    (state) => state + 1,
  );

  async function handleLike() {
    addOptimisticLike(null); // Instant UI update
    await likePost(); // Server function
  }

  return <button onClick={handleLike}>❤️ {optimisticLikes}</button>;
}
```

---

## 🛡️ Middleware

```tsx
// middleware.ts
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // ✅ Auth check
  const token = request.cookies.get("token");
  if (pathname.startsWith("/dashboard") && !token) {
    return NextResponse.redirect(new URL("/login", request.url));
  }

  // ✅ Add headers
  const response = NextResponse.next();
  response.headers.set("x-pathname", pathname);

  return response;
}

export const config = {
  matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};
```

---

## 🔌 API Routes

```tsx
// app/api/users/route.ts
import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams;
  const page = parseInt(searchParams.get("page") || "1");

  const users = await db.user.findMany({
    skip: (page - 1) * 10,
    take: 10,
  });

  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 });
}

// app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> },
) {
  const { id } = await params;
  const user = await db.user.findUnique({ where: { id } });

  if (!user) {
    return NextResponse.json({ error: "Not found" }, { status: 404 });
  }

  return NextResponse.json(user);
}
```

---

## 📊 Loading & Error States

```tsx
// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
      <div className="h-4 bg-gray-200 rounded w-full mb-2" />
      <div className="h-4 bg-gray-200 rounded w-3/4" />
    </div>
  );
}

// app/dashboard/error.tsx
("use client");

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="p-4 bg-red-50 rounded-lg">
      <h2 className="text-red-800 font-bold">Something went wrong!</h2>
      <p className="text-red-600">{error.message}</p>
      <button
        onClick={reset}
        className="mt-4 px-4 py-2 bg-red-600 text-white rounded"
      >
        Try again
      </button>
    </div>
  );
}
```

---

## ✅ Best Practices Checklist

### Architecture

- [ ] Server Components by default
- [ ] Client Components only for interactivity
- [ ] Feature-based file organization
- [ ] Parallel data fetching with Promise.all

### Performance

- [ ] Image optimization with next/image
- [ ] Font optimization with next/font
- [ ] Route prefetching enabled
- [ ] Suspense boundaries for streaming

### Security

- [ ] Environment variables for secrets
- [ ] Input validation with Zod
- [ ] CSRF protection in Server Functions
- [ ] Middleware for auth

### SEO

- [ ] Metadata configured per page
- [ ] OpenGraph images
- [ ] Sitemap generated
- [ ] robots.txt configured

---
