| name | dev-nextjs |
| description | Next.js development (App Router, Server Components, caching, streaming). Trigger when the user works with Next.js, modifies app/, pages/, next.config, or talks about RSC, Server Actions, Route Handlers, middleware. |
| allowed-tools | ["Read","Write","Edit","Bash","Glob","Grep"] |
| context | fork |
Next.js App Router
App Router vs Pages Router
App Router (default since Next 13, stable): app/ folder, Server Components by default, Server Actions, streaming. Prefer for any new project.
Pages Router: pages/ folder, getServerSideProps/getStaticProps. Legacy, do not add new routes there.
If the project has both, coexist — both can live together, but do not duplicate a route.
Server Components by default
Any component in app/ is a Server Component by default. It runs on the server, zero client JS.
export default async function PostsPage() {
const posts = await db.posts.findMany();
return <PostList posts={posts} />;
}
Switch to Client Component with "use client"
"use client";
import { useState } from "react";
export function SearchBox() {
const [query, setQuery] = useState("");
return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}
Rule: "use client" only if you need hooks (useState, useEffect) or browser events (onClick, onChange). Otherwise, stay Server Component.
Server/Client composition
Server Components can import Client Components, but the reverse is not allowed (except via children props).
export default async function Page() {
const data = await fetch(...);
return <ClientChart data={data} />;
}
"use client";
export function Layout({ children }: { children: React.ReactNode }) {
return <div>{children}</div>;
}
Data Fetching
Native fetch() with Next.js cache
const data = await fetch(url, { cache: "force-cache" });
const data = await fetch(url, { cache: "no-store" });
const data = await fetch(url, { next: { revalidate: 60 } });
const data = await fetch(url, { next: { tags: ["posts"] } });
IMPORTANT (Next 15+): fetch is no longer cached by default. You must explicitly set force-cache or next: { revalidate }.
Parallel data fetching
const user = await getUser();
const posts = await getPosts();
const [user, posts] = await Promise.all([getUser(), getPosts()]);
Server Actions
Functions executed on the server, invoked from the client without a manual API route.
"use server";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
await db.posts.create({ data: { title } });
revalidatePath("/posts");
}
import { createPost } from "../actions";
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" />
<button type="submit">Create</button>
</form>
);
}
Pitfalls:
- Always validate inputs with Zod (Server Actions receive unvalidated data)
- Always
revalidatePath or revalidateTag after a mutation
- Do NOT expose business logic without auth (check the session inside the action)
Route Handlers (API)
app/api/*/route.ts replaces pages/api/.
import { NextResponse } from "next/server";
export async function GET(request: Request) {
const posts = await db.posts.findMany();
return NextResponse.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.posts.create({ data: body });
return NextResponse.json(post, { status: 201 });
}
Streaming and Suspense
Show the page shell immediately, stream the slow content:
import { Suspense } from "react";
export default function Page() {
return (
<div>
<Header /> {/* Render immediately */}
<Suspense fallback={<PostsSkeleton />}>
<SlowPosts /> {/* Stream when ready */}
</Suspense>
</div>
);
}
loading.tsx
export default function Loading() {
return <PostsSkeleton />;
}
Middleware
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};
Pitfall: middleware runs on the Edge Runtime. No Node APIs (fs, crypto.createHash...) without a polyfill.
Metadata API
Replaces manual <head>.
export const metadata: Metadata = {
title: "My App",
description: "...",
};
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.id);
return { title: post.title };
}
Images and Fonts
import Image from "next/image";
import { Geist } from "next/font/google";
const geist = Geist({ subsets: ["latin"] });
<Image src="/hero.jpg" alt="" width={1200} height={600} priority />
Next loads and hosts fonts locally (no Google request), avoiding FOIT/FOUT.
Configuration
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
experimental: {
reactCompiler: true,
ppr: "incremental",
dynamicIO: true,
},
images: {
remotePatterns: [{ protocol: "https", hostname: "cdn.example.com" }],
},
};
export default nextConfig;
Vercel deployment
vercel — preview deploy
vercel --prod — production deploy
- Build settings auto-detected (npm, pnpm, bun)
- Env variables in the dashboard
Alternative: self-host with next build && next start (Node 18+).
Common pitfalls
| Problem | Solution |
|---|
| "use client" everywhere | Only add it to components that use hooks/events |
| Unwanted data refetch | Check cache and next.revalidate on the fetch |
| Build errors ERR_DYNAMIC | Mark the page export const dynamic = "force-dynamic" or fix dynamic calls |
| Slow middleware | Reduce the matcher, avoid fetches inside middleware |
| Hydration mismatch | No random/Date.now() in SSR without suppressHydrationWarning |
Verification
npm run build
npm run build -- --debug
npx @next/bundle-analyzer
Complements with the foundation
- Rule
.claude/rules/nextjs.md: path-specific rules (auto-activation on app/**)
- Rule
.claude/rules/performance.md: Core Web Vitals
- Skill
dev-react-perf: memoization, React lazy loading
- Skill
qa-chrome: visual audit of Next pages
Expected output
- App Router by default (not Pages Router unless partial migration)
- Server Components by default, "use client" only if necessary
- Explicit caching on every fetch (force-cache, no-store, or revalidate)
- Zod validation on Server Actions and Route Handlers
- Metadata API for SEO (never manual
<head> in App Router)
Rules
IMPORTANT: "use client" is the exception, not the rule. By default, everything is a Server Component.
IMPORTANT: Next 15+: fetch is no longer cached by default. Always specify the cache behavior.
IMPORTANT: Validate Server Action inputs with Zod before mutation.
YOU MUST use revalidatePath or revalidateTag after every mutation to invalidate the cache.
NEVER fetch inside middleware (Edge, slow).
NEVER expose business logic in a Route Handler without checking auth.
See also
Vercel Labs publishes official agent skills at vercel-labs/agent-skills (maintained by Vercel Engineering). Note the repo ships React + Vercel skills, not a dedicated Next.js one:
react-best-practices — 40+ rules across 8 categories from Vercel Engineering.
- Companion skills: React Composition Patterns, React View Transitions,
deploy-to-vercel, vercel-optimize, Web Design Guidelines.
Install the vendor skill alongside this one on a Next.js project: the vendor sharpens the React layer and Vercel deploy/optimize, while this foundation skill stays the primary Next.js reference — App Router, Server Components, caching/streaming, Server Actions, middleware, route handlers — plus the opinionated workflow the foundation imposes (TDD-first, security defaults, deploy-safety, anti-patterns).
Install command and full list of validated vendor skills: docs/recipes/recommended-vendor-skills.md. Audit pilot trace: specs/marketplace-audit/dev-skills-pilot-2026-05-05.md.