Some_claude_skills nextjs-app-router-expert
Expert in Next.js 14/15 App Router architecture, React Server Components (RSC), Server Actions, and modern full-stack React development. Specializes in routing patterns, data fetching strategies,
install
source · Clone the upstream repo
git clone https://github.com/curiositech/some_claude_skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/curiositech/some_claude_skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/nextjs-app-router-expert" ~/.claude/skills/curiositech-some-claude-skills-nextjs-app-router-expert && rm -rf "$T"
manifest:
.claude/skills/nextjs-app-router-expert/SKILL.mdsource content
Next.js App Router Expert
Overview
Expert in Next.js 14/15 App Router architecture, React Server Components (RSC), Server Actions, and modern full-stack React development. Specializes in routing patterns, data fetching strategies, caching, streaming, and deployment optimization.
When to Use
- Starting a new Next.js project with App Router
- Migrating from Pages Router to App Router
- Implementing complex routing patterns (parallel, intercepting routes)
- Optimizing data fetching with RSC and caching
- Setting up Server Actions for mutations
- Configuring middleware for auth/redirects
- Debugging hydration errors or RSC issues
- Deploying to Vercel, Cloudflare, or self-hosted
Capabilities
Routing Architecture
- File-based routing with
directoryapp/ - Dynamic routes (
,[slug]
,[...catchAll]
)[[...optional]] - Route groups
for organization(group) - Parallel routes
,@modal@sidebar - Intercepting routes
,(.)
,(..)(..)(..) - Loading and error boundaries per route segment
React Server Components
- Server vs Client component boundaries
directive placement'use client'- Composition patterns (server wrapping client)
- Streaming with Suspense boundaries
- Progressive rendering strategies
Data Fetching
with automatic deduplicationfetch()- Caching strategies (
,force-cache
,no-store
)revalidate
for static generationgenerateStaticParams()- Incremental Static Regeneration (ISR)
- On-demand revalidation with
/revalidatePath()revalidateTag()
Server Actions
- Form mutations with
'use server' - Optimistic updates with
useOptimistic - Progressive enhancement (works without JS)
- Error handling and validation
- Redirect after mutation
Middleware & Edge
for auth, redirects, rewritesmiddleware.ts- Edge Runtime vs Node.js Runtime
- Geolocation and conditional routing
- A/B testing and feature flags
Performance Optimization
- Image optimization with
next/image - Font optimization with
next/font - Script loading strategies
- Bundle analysis and code splitting
- Partial prerendering (PPR)
Dependencies
Works well with:
- React-specific performance patternsreact-performance-optimizer
- Vercel deployment configurationvercel-deployment
- Edge deployment patternscloudflare-worker-dev
- Database queries for RSCpostgresql-optimization
Examples
Basic Route Structure
app/ ├── layout.tsx # Root layout (required) ├── page.tsx # Home page (/) ├── loading.tsx # Loading UI ├── error.tsx # Error boundary ├── not-found.tsx # 404 page ├── blog/ │ ├── page.tsx # /blog │ └── [slug]/ │ ├── page.tsx # /blog/:slug │ └── loading.tsx # Per-route loading └── (auth)/ # Route group (no URL impact) ├── login/ │ └── page.tsx # /login └── register/ └── page.tsx # /register
Server Component with Data Fetching
// app/posts/page.tsx import { Suspense } from 'react'; async function getPosts() { const res = await fetch('https://api.example.com/posts', { next: { revalidate: 3600 }, // ISR: revalidate every hour }); return res.json(); } export default async function PostsPage() { const posts = await getPosts(); return ( <main> <h1>Blog Posts</h1> <Suspense fallback={<PostsSkeleton />}> <PostList posts={posts} /> </Suspense> </main> ); }
Server Action Form
// app/contact/page.tsx import { redirect } from 'next/navigation'; import { revalidatePath } from 'next/cache'; async function submitContact(formData: FormData) { 'use server'; const email = formData.get('email') as string; const message = formData.get('message') as string; // Validate if (!email || !message) { throw new Error('Email and message required'); } // Save to database await db.contacts.create({ email, message }); // Revalidate and redirect revalidatePath('/contact'); redirect('/contact/success'); } export default function ContactPage() { return ( <form action={submitContact}> <input name="email" type="email" required /> <textarea name="message" required /> <button type="submit">Send</button> </form> ); }
Parallel Routes (Modal Pattern)
app/ ├── layout.tsx ├── page.tsx ├── @modal/ │ ├── default.tsx # Empty state when no modal │ └── (.)photo/[id]/ │ └── page.tsx # Intercept /photo/[id] as modal └── photo/[id]/ └── page.tsx # Full page when direct navigation
// app/layout.tsx export default function Layout({ children, modal, }: { children: React.ReactNode; modal: React.ReactNode; }) { return ( <> {children} {modal} </> ); }
Middleware for Auth
// middleware.ts import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export function middleware(request: NextRequest) { const token = request.cookies.get('auth-token'); const isAuthPage = request.nextUrl.pathname.startsWith('/login'); const isProtectedPage = request.nextUrl.pathname.startsWith('/dashboard'); // Redirect authenticated users away from login if (isAuthPage && token) { return NextResponse.redirect(new URL('/dashboard', request.url)); } // Redirect unauthenticated users to login if (isProtectedPage && !token) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } export const config = { matcher: ['/dashboard/:path*', '/login'], };
Static Generation with Dynamic Params
// app/blog/[slug]/page.tsx import { notFound } from 'next/navigation'; export async function generateStaticParams() { const posts = await fetch('https://api.example.com/posts').then(r => r.json()); return posts.map((post: { slug: string }) => ({ slug: post.slug, })); } export async function generateMetadata({ params }: { params: { slug: string } }) { const post = await getPost(params.slug); return { title: post?.title ?? 'Post Not Found', description: post?.excerpt, }; } export default async function PostPage({ params }: { params: { slug: string } }) { const post = await getPost(params.slug); if (!post) { notFound(); } return ( <article> <h1>{post.title}</h1> {/* NOTE: Always sanitize HTML content with DOMPurify before rendering */} <div>{post.content}</div> </article> ); }
Streaming with Suspense
// app/dashboard/page.tsx import { Suspense } from 'react'; export default function DashboardPage() { return ( <div className="grid grid-cols-2 gap-4"> {/* These load in parallel and stream in as ready */} <Suspense fallback={<CardSkeleton />}> <RevenueCard /> </Suspense> <Suspense fallback={<CardSkeleton />}> <UsersCard /> </Suspense> <Suspense fallback={<TableSkeleton />}> <RecentOrders /> </Suspense> </div> ); } // Each component fetches its own data async function RevenueCard() { const revenue = await getRevenue(); // Server-side fetch return <Card title="Revenue" value={revenue} />; }
Best Practices
- Server Components by default - Only add
when needed for interactivity'use client' - Colocate data fetching - Fetch data in the component that needs it
- Use Suspense boundaries - Wrap async components for streaming
- Leverage caching - Use
and tags for efficient cachingrevalidate - Progressive enhancement - Server Actions work without JavaScript
- Route groups for organization - Use
to organize without affecting URLs(folder) - Error boundaries per segment - Add
to critical routeserror.tsx - Metadata API - Use
for dynamic SEOgenerateMetadata - Sanitize user content - Always use DOMPurify or similar when rendering HTML
Common Pitfalls
- Hydration mismatches - Server/client rendering differences (dates, random values)
- Over-using 'use client' - Pushing client boundary too high in the tree
- Waterfall fetching - Not parallelizing independent data fetches
- Missing loading states - Forgetting
or Suspense boundariesloading.tsx - Stale data - Not invalidating cache after mutations
- Large client bundles - Importing server-only code in client components
- XSS vulnerabilities - Rendering unsanitized HTML from user input