Next.js App Router Expert
SkillAI & modelsExpert 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,
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Next.js App Router Expert skill
What this skill tells your AI
The instructions your AI receives, as published by curiositech/some_claude_skills in .claude/skills/nextjs-app-router-expert/SKILL.md and read by ahel’s review.
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
app/directory - Dynamic routes (
[slug],[...catchAll],[[...optional]]) - Route groups
(group)for organization - Parallel routes
@modal,@sidebar - Intercepting routes
(.),(..),(..)(..) - Loading and error boundaries per route segment
React Server Components
- Server vs Client component boundaries
'use client'directive placement- Composition patterns (server wrapping client)
- Streaming with Suspense boundaries
- Progressive rendering strategies
Data Fetching
fetch()with automatic deduplication- Caching strategies (
force-cache,no-store,revalidate) generateStaticParams()for static generation- 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
middleware.tsfor auth, redirects, rewrites- 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-performance-optimizer- React-specific performance patternsvercel-deployment- Vercel deployment configurationcloudflare-worker-dev- Edge deployment patternspostgresql-optimization- Database queries for RSC
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
'use client'when needed for interactivity - Colocate data fetching - Fetch data in the component that needs it
- Use Suspense boundaries - Wrap async components for streaming
- Leverage caching - Use
revalidateand tags for efficient caching - Progressive enhancement - Server Actions work without JavaScript
- Route groups for organization - Use
(folder)to organize without affecting URLs - Error boundaries per segment - Add
error.tsxto critical routes - Metadata API - Use
generateMetadatafor dynamic SEO - 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
loading.tsxor Suspense boundaries - 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
Signals
- GitHub stars
- 219
- Forks
- 40
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
nextjs-app-router-expert- Source
- github.com/curiositech/some_claude_skills