# Nextjs

> Next.js rules - App Router, Server Components, Client Components ("use client"), Server Actions, data fetching and caching (SSG, ISR, revalidate, PPR), Route Handlers, proxy/middleware, next/image, next/font, Metadata API, Auth.js (NextAuth), i18n, generateStaticParams, layouts and templates, Turbopack, typedRoutes, deployment

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

---


# Next.js — Rules and Conventions

---

## 1. Philosophy

1. **Server Components by default** — Render on server, send HTML. Client Components only when needed.
2. **File-system routing** — App Router = folders = routes. Colocate files.
3. **Streaming first** — Suspense boundaries, progressive rendering, no waterfall.
4. **Caching aggressive** — Fetch + Next.js cache = fast by default. Revalidate on demand.
5. **TypeScript strict** — `strict: true`, typed routes, typed API responses.

---

## 2. Version Baseline

| Technology | Minimum Version |
| ---------- | --------------- |
| Next.js    | 14.2+ (15 RC)   |
| React      | 18.3+           |
| Node.js    | 22+             |
| TypeScript | 5.4+            |

---

## 3. Setup & Project Structure

```bash
pnpm create next-app@latest my-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
```

```text
src/
├── app/                    # App Router routes
│   ├── (auth)/             # Route groups
│   ├── (dashboard)/
│   ├── api/                # Route Handlers
│   ├── layout.tsx          # Root layout
│   ├── page.tsx            # Home page
│   └── globals.css
├── components/             # Shared components
├── lib/                    # Utilities, DB, auth config
├── types/                  # Global types
├── middleware.ts           # Middleware
├── next.config.ts
└── tsconfig.json
```

---

## 4. Routing — App Router

### File conventions

| File            | Purpose                     |
| --------------- | --------------------------- |
| `page.tsx`      | Route UI (Server Component) |
| `layout.tsx`    | Shared UI + wraps children  |
| `loading.tsx`   | Streaming fallback          |
| `error.tsx`     | Error boundary              |
| `not-found.tsx` | 404 UI                      |
| `route.ts`      | Route Handler (API)         |
| `template.tsx`  | Re-rendered layout          |
| `default.tsx`   | Parallel route fallback     |

### Dynamic routes

```text
app/
├── users/
│   ├── [id]/
│   │   ├── page.tsx          # /users/123
│   │   └── settings/page.tsx # /users/123/settings
│   └── page.tsx              # /users
```

### Route Groups (organization only)

```text
app/
├── (marketing)/
│   ├── page.tsx              # /
│   └── about/page.tsx        # /about
├── (dashboard)/
│   ├── layout.tsx            # Dashboard layout
│   └── page.tsx              # /dashboard
```

---

## 5. Server vs Client Components

### Default: Server Component

```tsx
// app/users/page.tsx (Server Component)
import { getUsers } from "@/lib/db";

export default async function UsersPage() {
  const users = await getUsers(); // Direct DB access
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}
```

### Client Component (when needed)

```tsx
// app/components/Search.tsx
"use client";

import { useState } from "react";

export function Search() {
  const [query, setQuery] = useState("");
  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
```

### When to use `'use client'`

| Need                                    | Use Client  |
| --------------------------------------- | ----------- |
| `useState`, `useEffect`, `useRef`       | ✅          |
| Event handlers (`onClick`, `onChange`)  | ✅          |
| Browser APIs (`window`, `localStorage`) | ✅          |
| Custom hooks using above                | ✅          |
| Data fetching only                      | ❌ (Server) |
| Direct DB/ORM access                    | ❌ (Server) |

### Composition pattern

```tsx
// Server Component (default)
import { ClientSearch } from "./ClientSearch";

export default function Page() {
  return (
    <section>
      <h1>Users</h1>
      <ClientSearch /> {/* Interactive island */}
    </section>
  );
}
```

---

## 6. Data Fetching

### Fetch with caching (default)

```tsx
// Cached indefinitely (build time)
async function getUsers() {
  const res = await fetch("https://api.example.com/users", {
    cache: "force-cache",
  });
  return res.json();
}

// Revalidate every 60s
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { revalidate: 60 },
  });
  return res.json();
}

// No cache (dynamic)
async function getSession() {
  const res = await fetch("https://api.example.com/session", {
    cache: "no-store",
  });
  return res.json();
}
```

### Parallel vs Sequential

```tsx
// ✅ Parallel (fast)
const [users, posts] = await Promise.all([getUsers(), getPosts()]);

// ❌ Sequential (slow)
const users = await getUsers();
const posts = await getPosts();
```

### `generateStaticParams` (SSG)

```tsx
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await getPosts();
  return posts.map((p) => ({ slug: p.slug }));
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <article>{post.content}</article>;
}
```

---

## 7. Caching & Revalidation

### Cache layers

| Layer                | Scope                            | Control                           |
| -------------------- | -------------------------------- | --------------------------------- |
| **Data Cache**       | `fetch` + `next: { revalidate }` | `revalidateTag`, `revalidatePath` |
| **Full Route Cache** | Static pages                     | `revalidatePath`                  |
| **Router Cache**     | Client navigation                | Automatic                         |

### Invalidate on mutation

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

import { revalidateTag, revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  await db.posts.create({ data: { title: formData.get("title") } });

  revalidateTag("posts"); // Invalidate fetch with tag
  revalidatePath("/blog"); // Invalidate route
  redirect("/blog");
}
```

```tsx
// Tagged fetch
async function getPosts() {
  const res = await fetch("https://api.example.com/posts", {
    next: { tags: ["posts"] },
  });
  return res.json();
}
```

### Rules

- **Default: cached** — `fetch` = cached unless `no-store`
- **Tag-based invalidation** — prefer over path
- **Server Actions** — mutate + revalidate in one call

---

## 8. Server Actions

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

import { revalidateTag } from "next/cache";
import { z } from "zod";

const schema = z.object({ title: z.string().min(3) });

export async function createPost(prev: any, formData: FormData) {
  const result = schema.safeParse(Object.fromEntries(formData));
  if (!result.success) return { errors: result.error.flatten() };

  await db.posts.create({ data: result.data });
  revalidateTag("posts");
  redirect("/blog");
}
```

```tsx
// Component
import { createPost } from "./actions";

export function CreateForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <button type="submit">Create</button>
    </form>
  );
}
```

### Rules Sever Actions

- **`'use server'`** at top of file or function
- **Zod validation** — type-safe input
- **Return redirect or error** — no JSON response
- **Combine with `revalidateTag`/`revalidatePath`**

---

## 9. Streaming, Suspense & PPR

### Streaming with Suspense

```tsx
// app/dashboard/page.tsx
import { Suspense } from "react";
import { SlowWidget } from "@/components/SlowWidget";
import { Skeleton } from "@/components/Skeleton";

export default function Dashboard() {
  return (
    <section>
      <h1>Dashboard</h1>
      <Suspense fallback={<Skeleton />}>
        <SlowWidget />
      </Suspense>
    </section>
  );
}
```

### Partial Prerendering (PPR) — Next.js 15+

```tsx
// next.config.ts
export default {
  experimental: { ppr: "incremental" },
};
```

```tsx
// app/dashboard/page.tsx
export const experimental_ppr = true;

export default function Dashboard() {
  return (
    <>
      <StaticHeader /> {/* Prerendered */}
      <Suspense fallback={<Skeleton />}>
        <DynamicWidget /> {/* Streamed */}
      </Suspense>
    </>
  );
}
```

---

## 10. Route Handlers (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 limit = searchParams.get("limit") || "20";

  const users = await getUsers({ limit: Number(limit) });
  return NextResponse.json(users);
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await createUser(body);
  return NextResponse.json(user, { status: 201 });
}
```

### Rules API Routes

- **`NextRequest`/`NextResponse`** — standard Web APIs
- **`request.json()`** — parse JSON body
- **`searchParams`** — URL query params
- **CORS** — handled by middleware or headers

---

## 11. Middleware

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

export function middleware(request: NextRequest) {
  const token = request.cookies.get("session")?.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/:path*"],
};
```

### Rules Middleware

- **Edge runtime** — fast, limited APIs
- **Return `NextResponse`** — redirect, rewrite, headers
- **`config.matcher`** — limit scope
- **No heavy computation** — keep fast

---

## 12. Image & Font Optimization

### `next/image`

```tsx
import Image from "next/image";

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="Hero"
      width={1200}
      height={600}
      priority // LCP image
      placeholder="blur"
      blurDataURL="data:image/..."
      sizes="(max-width: 768px) 100vw, 50vw"
    />
  );
}
```

### `next/font`

```tsx
// app/layout.tsx
import { Inter, Roboto_Mono } from "next/font/google";

const inter = Inter({ subsets: ["latin"], variable: "--font-inter" });
const mono = Roboto_Mono({ subsets: ["latin"], variable: "--font-mono" });

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${mono.variable}`}>
      <body>{children}</body>
    </html>
  );
}
```

### Rules Image & Font optimization

- **`priority`** on LCP image
- **`sizes`** for responsive images
- **Variable fonts** — single file, all weights
- **`font-display: swap`** — automatic

---

## 13. Metadata API

```tsx
// app/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'Best app ever',
  openGraph: {
    title: 'My App',
    images: ['/og.png']
  },
  twitter: { card: 'summary_large_image' },
  robots: { index: true, follow: true }
}

export default function Page() { ... }
```

### Dynamic metadata

```tsx
// app/blog/[slug]/page.tsx
export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.coverImage] },
  };
}
```

---

## 14. Authentication

> **Auth patterns**: see `auth` skill.

### NextAuth.js (Auth.js) v5

```tsx
// lib/auth.ts
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";

export const { handlers, signIn, signOut, auth } = NextAuth({
  providers: [
    Credentials({
      credentials: { email: {}, password: {} },
      authorize: async (creds) => {
        /* verify */
      },
    }),
  ],
  callbacks: {
    jwt: ({ token, user }) => {
      if (user) token.id = user.id;
      return token;
    },
    session: ({ session, token }) => {
      session.user.id = token.id;
      return session;
    },
  },
});
```

```tsx
// app/api/auth/[...nextauth]/route.ts
export const { GET, POST } = handlers;
```

### Server Component access

```tsx
// Server Component
import { auth } from "@/lib/auth";

export default async function Page() {
  const session = await auth();
  return <div>Welcome {session?.user?.email}</div>;
}
```

> **Full patterns**: see `auth` skill.

---

## 15. Internationalization (i18n)

### Config

```ts
// next.config.ts
export default {
  i18n: {
    locales: ["en", "es", "fr"],
    defaultLocale: "en",
    localeDetection: true,
  },
};
```

### Routing

```text
app/
├── [locale]/
│   ├── layout.tsx
│   ├── page.tsx
│   └── blog/
│       └── [slug]/page.tsx
```

```tsx
// Middleware for locale prefix
import { createMiddleware } from "next-intl/middleware";
export default createMiddleware({ locales: ["en", "es"], defaultLocale: "en" });
export const config = { matcher: ["/((?!api|_next|_vercel|.*\\..*).*)"] };
```

---

## 16. Configuration (`next.config.ts`)

```ts
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactStrictMode: true,
  typedRoutes: true,
  experimental: {
    ppr: "incremental",
    serverActions: { bodySizeLimit: "2mb" },
  },
  images: {
    remotePatterns: [{ protocol: "https", hostname: "cdn.example.com" }],
  },
  async headers() {
    return [{ source: "/:path*", headers: securityHeaders() }];
  },
};

export default nextConfig;
```

---

## 17. TypeScript Specifics

### Typed Routes

```ts
// Enable in next.config.ts: typedRoutes: true

// Auto-generated: next-route-types.d.ts
// <Link href="/blog/[slug]" />  // Type-safe
// router.push('/blog/invalid')  // Type error
```

### API Types

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

type User = { id: string; name: string }

export async function GET(): Promise<NextResponse<User[]>> { ... }
export async function POST(req: NextRequest): Promise<NextResponse<User>> { ... }
```

---

## 18. Methodology

Before using ANY Next.js pattern not documented in
this skill:

1. **MCP Context7** (priority): `context7_resolve-library-id` +
   `context7_query-docs` for Next.js.
2. **Official docs**: nextjs.org — verify current APIs + features.
3. **Project config**: `next.config.ts`, `tsconfig.json`,
   `middleware.ts` — verify against actual setup.
4. **HARD RULE**: If not in this skill AND cannot be verified against
   2 authoritative sources → DO NOT USE IT. Document as assumption or risk in
   report to orchestrator.

---

## 19. Prohibitions

- ❌ Do not use Pages Router for new routes — App Router only
- ❌ Do not use `getServerSideProps`/`getStaticProps` — App Router patterns
- ❌ Do not fetch in Client Components — Server Components + props
- ❌ Do not skip `'use client'` when using hooks/events
- ❌ Do not use `cache: 'no-store'` everywhere — default is fine
- ❌ Do not skip `revalidateTag` after mutations
- ❌ Do not use `dangerouslySetInnerHTML` — CSP blocks
- ❌ Do not disable `reactStrictMode` or `typedRoutes`

---

## 20. References

> **Note:** For React patterns, see [React](../reactjs/SKILL.md)
> **Note:** For HTML conventions, see [HTML](../html/SKILL.md)
> **Note:** For CSS conventions, see [CSS](../css/SKILL.md)
> **Note:** For JavaScript conventions, see [JavaScript](../javascript/SKILL.md)
> **Note:** For TypeScript rules, see [TypeScript](../typescript/SKILL.md)
> **Note:** For Testing patterns, see [Testing](../testing/SKILL.md)
> **Note:** For Deployment, see [Deploy](../deploy/SKILL.md)
> **Note:** For Security, see [Security](../security/SKILL.md)
> **Note:** For Performance, see [Performance](../performance/SKILL.md)
> **Note:** For Auth patterns, see [Auth](../auth/SKILL.md)

---

Last updated: 2026-08

