Next.js — Rules and Conventions
1. Philosophy
- Server Components by default — Render on server, send HTML. Client Components only when needed.
- File-system routing — App Router = folders = routes. Colocate files.
- Streaming first — Suspense boundaries, progressive rendering, no waterfall.
- Caching aggressive — Fetch + Next.js cache = fast by default. Revalidate on demand.
- 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
pnpm create next-app@latest my-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
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
app/
├── users/
│ ├── [id]/
│ │ ├── page.tsx # /users/123
│ │ └── settings/page.tsx # /users/123/settings
│ └── page.tsx # /users
Route Groups (organization only)
app/
├── (marketing)/
│ ├── page.tsx # /
│ └── about/page.tsx # /about
├── (dashboard)/
│ ├── layout.tsx # Dashboard layout
│ └── page.tsx # /dashboard
5. Server vs Client Components
Default: Server Component
// 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)
// app/components/Search.tsx
"use client";
import { useState } from "react";
export function Search() {
const [query, setQuery] = useState("");
return <input value={query} => 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
// 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)
// 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
// ✅ Parallel (fast)
const [users, posts] = await Promise.all([getUsers(), getPosts()]);
// ❌ Sequential (slow)
const users = await getUsers();
const posts = await getPosts();
generateStaticParams (SSG)
// 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
// 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");
}
// 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 unlessno-store - Tag-based invalidation — prefer over path
- Server Actions — mutate + revalidate in one call
8. Server Actions
// 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");
}
// 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
// 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+
// next.config.ts
export default {
experimental: { ppr: "incremental" },
};
// 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)
// 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 APIsrequest.json()— parse JSON bodysearchParams— URL query params- CORS — handled by middleware or headers
11. Middleware
// 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
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
// 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
priorityon LCP imagesizesfor responsive images- Variable fonts — single file, all weights
font-display: swap— automatic
13. Metadata API
// 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
// 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
authskill.
NextAuth.js (Auth.js) v5
// 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;
},
},
});
// app/api/auth/[...nextauth]/route.ts
export const { GET, POST } = handlers;
Server Component access
// 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
authskill.
15. Internationalization (i18n)
Config
// next.config.ts
export default {
i18n: {
locales: ["en", "es", "fr"],
defaultLocale: "en",
localeDetection: true,
},
};
Routing
app/
├── [locale]/
│ ├── layout.tsx
│ ├── page.tsx
│ └── blog/
│ └── [slug]/page.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)
// 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
// 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
// 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:
- MCP Context7 (priority):
context7_resolve-library-id+context7_query-docsfor Next.js. - Official docs: nextjs.org — verify current APIs + features.
- Project config:
next.config.ts,tsconfig.json,middleware.ts— verify against actual setup. - 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
revalidateTagafter mutations - ❌ Do not use
dangerouslySetInnerHTML— CSP blocks - ❌ Do not disable
reactStrictModeortypedRoutes
20. References
Note: For React patterns, see React Note: For HTML conventions, see HTML Note: For CSS conventions, see CSS Note: For JavaScript conventions, see JavaScript Note: For TypeScript rules, see TypeScript Note: For Testing patterns, see Testing Note: For Deployment, see Deploy Note: For Security, see Security Note: For Performance, see Performance Note: For Auth patterns, see Auth
Last updated: 2026-08