Next.js Expert Skill
Expert-level Next.js 14+ patterns, App Router, Server Components, data fetching, and optimization.
Auto-Detection
This skill activates when:
-
Working with Next.js projects
-
Detected next in package.json
-
Working with App Router structure (app/ directory)
-
Building API routes or Server Components
- App Router Structure
Recommended Structure
app/ ├── (auth)/ # Route group (no URL impact) │ ├── login/ │ │ └── page.tsx │ └── register/ │ └── page.tsx ├── (dashboard)/ │ ├── layout.tsx # Shared dashboard layout │ ├── page.tsx # /dashboard │ └── settings/ │ └── page.tsx # /dashboard/settings ├── api/ │ └── users/ │ └── route.ts # API route ├── layout.tsx # Root layout ├── page.tsx # Home page ├── loading.tsx # Loading UI ├── error.tsx # Error UI ├── not-found.tsx # 404 page └── global-error.tsx # Global error boundary
File Conventions
file_conventions[8]{file,purpose}: page.tsx,Route UI component layout.tsx,Shared layout (preserves state) template.tsx,Shared layout (re-renders) loading.tsx,Loading UI (Suspense) error.tsx,Error boundary not-found.tsx,404 page route.ts,API endpoint middleware.ts,Request middleware
- Server vs Client Components
Server Components (Default)
// ✅ GOOD - Server Component (default, no directive needed) // app/users/page.tsx import { db } from '@/lib/db';
export default async function UsersPage() { // Direct database access - runs on server only const users = await db.user.findMany();
return ( <ul> {users.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }
Client Components
// ✅ GOOD - Client Component (explicit directive) 'use client';
import { useState } from 'react';
export function Counter() { const [count, setCount] = useState(0);
return ( <button onClick={() => setCount(c => c + 1)}> Count: {count} </button> ); }
Component Boundaries
// ✅ GOOD - Keep client boundary as low as possible // app/dashboard/page.tsx (Server Component) import { getUser } from '@/lib/auth'; import { UserProfile } from './UserProfile'; import { InteractiveChart } from './InteractiveChart'; // Client
export default async function DashboardPage() { const user = await getUser();
return ( <div> {/* Server Component - no JS shipped */} <UserProfile user={user} />
{/* Client Component - only this ships JS */}
<InteractiveChart data={user.stats} />
</div>
); }
- Data Fetching
Server Component Fetching
// ✅ GOOD - Fetch in Server Component async function getData() { const res = await fetch('https://api.example.com/data', { // Cache options cache: 'force-cache', // Default - cached indefinitely // cache: 'no-store', // No caching // next: { revalidate: 60 }, // Revalidate every 60s // next: { tags: ['posts'] }, // Tag-based revalidation });
if (!res.ok) { throw new Error('Failed to fetch data'); }
return res.json(); }
export default async function Page() { const data = await getData(); return <div>{/* render data */}</div>; }
Parallel Data Fetching
// ✅ GOOD - Parallel fetching export default async function Page() { // Start both fetches simultaneously const userPromise = getUser(); const postsPromise = getPosts();
// Wait for both const [user, posts] = await Promise.all([userPromise, postsPromise]);
return ( <div> <UserProfile user={user} /> <PostList posts={posts} /> </div> ); }
Streaming with Suspense
// ✅ GOOD - Stream slow data import { Suspense } from 'react';
export default function Page() { return ( <div> {/* Renders immediately */} <Header />
{/* Streams when ready */}
<Suspense fallback={<PostsSkeleton />}>
<Posts />
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments />
</Suspense>
</div>
); }
// Async Server Component async function Posts() { const posts = await getPosts(); // Slow fetch return <PostList posts={posts} />; }
- Server Actions
Form Actions
// ✅ GOOD - Server Action in separate file // app/actions.ts 'use server';
import { revalidatePath } from 'next/cache'; import { redirect } from 'next/navigation'; import { z } from 'zod';
const CreatePostSchema = z.object({ title: z.string().min(1), content: z.string().min(10), });
export async function createPost(formData: FormData) { const validatedFields = CreatePostSchema.safeParse({ title: formData.get('title'), content: formData.get('content'), });
if (!validatedFields.success) { return { errors: validatedFields.error.flatten().fieldErrors, }; }
const { title, content } = validatedFields.data;
await db.post.create({ data: { title, content } });
revalidatePath('/posts'); redirect('/posts'); }
// ✅ GOOD - Form with Server Action // app/posts/new/page.tsx import { createPost } from '@/app/actions';
export default function NewPostPage() { return ( <form action={createPost}> <input name="title" placeholder="Title" required /> <textarea name="content" placeholder="Content" required /> <SubmitButton /> </form> ); }
// Client component for pending state 'use client'; import { useFormStatus } from 'react-dom';
function SubmitButton() { const { pending } = useFormStatus(); return ( <button type="submit" disabled={pending}> {pending ? 'Creating...' : 'Create Post'} </button> ); }
useActionState (React 19)
'use client';
import { useActionState } from 'react'; import { createPost } from '@/app/actions';
export function CreatePostForm() { const [state, formAction, isPending] = useActionState(createPost, null);
return ( <form action={formAction}> <input name="title" /> {state?.errors?.title && <p>{state.errors.title}</p>}
<textarea name="content" />
{state?.errors?.content && <p>{state.errors.content}</p>}
<button disabled={isPending}>
{isPending ? 'Creating...' : 'Create'}
</button>
</form>
); }
- API Routes
Route Handlers
// app/api/users/route.ts import { NextRequest, NextResponse } from 'next/server';
// GET /api/users export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const page = searchParams.get('page') ?? '1';
const users = await db.user.findMany({ skip: (parseInt(page) - 1) * 10, take: 10, });
return NextResponse.json(users); }
// POST /api/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 Routes
// app/api/users/[id]/route.ts import { NextRequest, NextResponse } from 'next/server';
type Params = { params: { id: string } };
export async function GET(request: NextRequest, { params }: Params) { const user = await db.user.findUnique({ where: { id: params.id }, });
if (user == null) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); }
return NextResponse.json(user); }
export async function DELETE(request: NextRequest, { params }: Params) { await db.user.delete({ where: { id: params.id } }); return new NextResponse(null, { status: 204 }); }
- Middleware
// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) { // Check auth const token = request.cookies.get('token');
if (token == null && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); }
// Add headers const response = NextResponse.next(); response.headers.set('x-request-id', crypto.randomUUID());
return response; }
export const config = { matcher: [ // Match all paths except static files '/((?!_next/static|_next/image|favicon.ico).*)', ], };
- Metadata & SEO
Static Metadata
// app/page.tsx import type { Metadata } from 'next';
export const metadata: Metadata = { title: 'Home | My App', description: 'Welcome to my app', openGraph: { title: 'Home | My App', description: 'Welcome to my app', images: ['/og-image.png'], }, };
export default function HomePage() { return <main>...</main>; }
Dynamic Metadata
// app/posts/[slug]/page.tsx import type { Metadata } from 'next';
type Props = { params: { slug: string } };
export async function generateMetadata({ params }: Props): Promise<Metadata> { const post = await getPost(params.slug);
return { title: post.title, description: post.excerpt, openGraph: { title: post.title, description: post.excerpt, images: [post.coverImage], }, }; }
export default async function PostPage({ params }: Props) { const post = await getPost(params.slug); return <article>{/* ... */}</article>; }
- Caching & Revalidation
Cache Strategies
cache_strategies[4]{strategy,use_case,code}: Static,Rarely changes,cache: 'force-cache' Time-based,Updates periodically,next: { revalidate: 60 } On-demand,User-triggered,revalidatePath() / revalidateTag() No cache,Always fresh,cache: 'no-store'
On-Demand Revalidation
// app/actions.ts 'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function updatePost(id: string, data: PostData) { await db.post.update({ where: { id }, data });
// Revalidate specific path
revalidatePath(/posts/${id});
// Or revalidate by tag revalidateTag('posts'); }
// In fetch const posts = await fetch('https://api.example.com/posts', { next: { tags: ['posts'] }, });
- Image Optimization
import Image from 'next/image';
// ✅ GOOD - Optimized image <Image src="/hero.jpg" alt="Hero image" width={1200} height={600} priority // Load immediately (LCP) placeholder="blur" blurDataURL="data:image/jpeg;base64,..." />
// ✅ GOOD - Fill container <div className="relative h-64 w-full"> <Image src={post.coverImage} alt={post.title} fill sizes="(max-width: 768px) 100vw, 50vw" className="object-cover" /> </div>
// ✅ GOOD - Remote images (configure in next.config.js) // next.config.js module.exports = { images: { remotePatterns: [ { protocol: 'https', hostname: 'cdn.example.com' }, ], }, };
- Error Handling
// app/error.tsx (Client Component required) 'use client';
export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return ( <div> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </div> ); }
// app/global-error.tsx (Root layout errors) 'use client';
export default function GlobalError({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return ( <html> <body> <h2>Something went wrong!</h2> <button onClick={() => reset()}>Try again</button> </body> </html> ); }
Quick Reference
checklist[12]{pattern,best_practice}: Components,Server by default Client when needed Client directive,'use client' at top of file Data fetching,Fetch in Server Components Parallel fetch,Promise.all for multiple fetches Streaming,Suspense for slow data Forms,Server Actions + useFormStatus API routes,Route handlers in app/api Caching,Tag-based revalidation Images,next/image with sizes prop Metadata,generateMetadata for dynamic Errors,error.tsx at route level Loading,loading.tsx for Suspense
Version: 1.3.0