skills/nextjs-app-router-expert/SKILL.md
--- license: Apache-2.0 name: nextjs-app-router-expert version: 1.0.0 category: Frontend & UI tags: - nextjs - react - app-router - rsc - server-components - full-stack --- # 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. ## Decision Points ### Ro
npx skillsauth add curiositech/windags-skills nextjs-app-router-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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.
Is the UI requirement:
├── Simple nested layouts?
│ └── Use standard file-based routing (page.tsx, layout.tsx)
│ └── Check: Does each segment need its own loading/error states?
│ ├── Yes → Add loading.tsx/error.tsx per segment
│ └── No → Use parent boundaries only
├── Content that appears alongside main page?
│ └── Use parallel routes (@slot)
│ └── Check: Should content persist across navigation?
│ ├── Yes → Use default.tsx to maintain state
│ └── No → Let it unmount naturally
├── Modal/overlay that intercepts navigation?
│ └── Use intercepting routes (.)
│ └── Check: What's the fallback for direct access?
│ ├── Same content → Create both routes with shared component
│ └── Different UX → Create separate page implementations
└── Dynamic segments with complex patterns?
└── Use catch-all routes ([...slug])
└── Check: Are some segments optional?
├── Yes → Use [[...optional]] syntax
└── No → Use [...required] syntax
What's the data freshness requirement:
├── Static at build time?
│ └── Use generateStaticParams() + fetch with force-cache
├── Fresh on every request?
│ └── Use fetch with no-store or dynamic functions
├── Cached with periodic updates?
│ └── Use fetch with revalidate: seconds
└── User-specific but cacheable?
└── Use fetch with cache + cookies() to make dynamic
Does the component need:
├── Browser APIs (localStorage, window)?
│ └── Use 'use client' at component level
├── Event handlers (onClick, onSubmit)?
│ └── Use 'use client' at component level
├── React hooks (useState, useEffect)?
│ └── Use 'use client' at component level
└── Only data fetching and rendering?
└── Keep as Server Component
└── Check: Do children need client features?
├── Yes → Pass server data as props to client children
└── No → Keep entire tree as server components
Symptoms: Console errors "Text content did not match", layout shifts on page load Diagnosis: Server-rendered HTML differs from client-rendered HTML Fix:
Symptoms: Large bundle sizes, slow initial page loads, excessive JavaScript Diagnosis: 'use client' placed too high in component tree, pulling server logic to client Fix:
Symptoms: Stale data after mutations, users see outdated content Diagnosis: Missing revalidatePath/revalidateTag after Server Actions Fix:
Symptoms: Slow page loads, sequential loading indicators, poor Core Web Vitals Diagnosis: Sequential data fetching instead of parallel, no Suspense boundaries Fix:
Symptoms: Confusing URLs, components in wrong places, hard to navigate codebase Diagnosis: Mixing logical grouping with URL structure, missing route groups Fix:
// Decision: Product data is semi-static, reviews are dynamic
// Solution: ISR for product, streaming for reviews
// app/products/[id]/page.tsx
import { Suspense } from 'react';
// Static product data with 1-hour revalidation
async function getProduct(id: string) {
const res = await fetch(`https://api.shop.com/products/${id}`, {
next: { revalidate: 3600, tags: ['product'] }
});
if (!res.ok) throw new Error('Product not found');
return res.json();
}
// Dynamic reviews, always fresh
async function getReviews(id: string) {
const res = await fetch(`https://api.shop.com/products/${id}/reviews`, {
cache: 'no-store'
});
return res.json();
}
export default async function ProductPage({ params }: { params: { id: string } }) {
// Expert catches: Fetch product immediately, stream reviews
// Novice misses: Would wait for all data before rendering
const product = await getProduct(params.id);
return (
<main>
<ProductDetails product={product} />
<AddToCartForm productId={params.id} />
{/* Stream reviews while showing product immediately */}
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewsSection productId={params.id} />
</Suspense>
</main>
);
}
async function ReviewsSection({ productId }: { productId: string }) {
const reviews = await getReviews(productId);
return <Reviews data={reviews} />;
}
// Server Action for cart - expert includes optimistic update
async function addToCart(formData: FormData) {
'use server';
const productId = formData.get('productId') as string;
// ... save to database
revalidateTag('cart'); // Invalidate cart count
redirect('/cart');
}
Expert Decision Points Navigated:
Don't use this skill for:
react-spa-development insteadnextjs-pages-router insteadzustand-state-management insteadwebsocket-integration insteadnextauth-integration insteadpostgresql-optimization insteadvercel-deployment or docker-deployment insteadDelegate to other skills when:
react-performance-optimizerreact-hook-form-experttypescript-advanced-patternstools
Building resilient distributed systems with circuit breakers, retries with full-jitter exponential backoff, retry budgets (per-request 3-attempt + per-client 10% ratio per Google SRE), deadline propagation, and the cascading-failure math (4 layers × 3 retries = 64x amplification). Grounded in Resilience4j, Microsoft Cloud Patterns, AWS Architecture Blog (Marc Brooker), and Google SRE Book.
testing
Designing HTTP cache headers that work correctly across browsers, CDNs, and shared proxies — `Cache-Control` directives per RFC 9111, `stale-while-revalidate` and `stale-if-error` per RFC 5861, the Vary header for varying responses, and surrogate keys for tag-based purging. Grounded in IETF RFCs and Cloudflare/Fastly docs.
development
Use when designing or fixing a Content Security Policy on a real site, choosing between nonce-based and hash-based CSP, adding strict-dynamic, debugging "Refused to execute inline script" errors, deploying CSP in report-only mode first, configuring report-to / report-uri, or auditing an existing policy for unsafe-inline / unsafe-eval / wildcards. Triggers: "CSP blocks legitimate inline script", strict-dynamic, nonce-{RANDOM}, sha256-{HASH}, object-src none, base-uri none, frame-ancestors, Trusted Types, X-Content-Security-Policy obsolete, report-only vs enforced. NOT for general HTTP security headers (HSTS, COOP/COEP), Trusted Types deep dive, CORS configuration, or building a WAF.
tools
Choosing and operating an HTTP API versioning strategy that doesn't break clients — Stripe's date-based pinned versions, the Deprecation/Sunset header pair (RFC 9745 + RFC 8594), URI vs header vs media-type approaches, and the version-transformer pattern. Grounded in Stripe's published architecture and IETF RFCs.