
Nextjs App Router Patterns
- 25.6k installs
- 38.3k repo stars
- Updated July 22, 2026
- wshobson/agents
wshobson/agents is a multi-harness agentic plugin marketplace with 92 plugins, 199 agents, and 162 skills.
About
A production-ready agentic plugin marketplace with 92 plugins spanning architecture, languages, security, data, and infrastructure. It generates idiomatic artifacts for six different agent harnesses (Claude Code, Codex, Cursor, OpenCode, Gemini, Copilot) from a single Markdown source. Each plugin is isolated and composable; installing a plugin loads only its components (agents, commands, skills) into context.
- Massive agentic plugin marketplace: 92 plugins, 199 agents, 162 skills, 106 commands for multi-harness use
- Multi-harness native generation: Claude Code, Codex CLI, Cursor, OpenCode, Gemini CLI, Copilot - each gets idiomatic art
- Domain-expert agents for architecture, languages (Python, JS, Rust, Go, Java), infra, security, data, ML, docs, business
Nextjs App Router Patterns by the numbers
- 25,631 all-time installs (skills.sh)
- +764 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #24 of 2,277 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
nextjs-app-router-patterns capabilities & compatibility
- Use cases
- api development · debugging · refactoring
npx skills add https://github.com/wshobson/agents --skill nextjs-app-router-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 25.6k |
|---|---|
| repo stars | ★ 38.3k |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 22, 2026 |
| Repository | wshobson/agents ↗ |
How do you structure Next.js App Router data fetching correctly?
Domain-expert plugins and multi-agent coordination for full-stack, security, ML, and DevOps workflows across six agent harnesses.
Who is it for?
Frontend developers building Next.js App Router pages who need proven Server Component, Suspense, and data-fetching patterns with worked TypeScript examples.
Skip if: Developers on Next.js Pages Router, plain React SPAs, or teams that only need deployment or CI configuration guidance.
When should I use this skill?
The user is implementing Next.js App Router pages with Server Components, streaming, Suspense, or searchParams-based data fetching.
What you get
TypeScript page and component patterns for Server Components, Suspense boundaries, and searchParams-driven revalidation.
- Server Component page patterns
- Suspense streaming layouts
- searchParams data-fetching examples
By the numbers
- Includes worked TypeScript patterns for app/products/page.tsx with Suspense and searchParams
Files
Next.js App Router Patterns
Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.
When to Use This Skill
- Building new Next.js applications with App Router
- Migrating from Pages Router to App Router
- Implementing Server Components and streaming
- Setting up parallel and intercepting routes
- Optimizing data fetching and caching
- Building full-stack features with Server Actions
Core Concepts
1. Rendering Modes
| Mode | Where | When to Use |
|---|---|---|
| Server Components | Server only | Data fetching, heavy computation, secrets |
| Client Components | Browser | Interactivity, hooks, browser APIs |
| Static | Build time | Content that rarely changes |
| Dynamic | Request time | Personalized or real-time data |
| Streaming | Progressive | Large pages, slow data sources |
2. File Conventions
app/
├── layout.tsx # Shared UI wrapper
├── page.tsx # Route UI
├── loading.tsx # Loading UI (Suspense)
├── error.tsx # Error boundary
├── not-found.tsx # 404 UI
├── route.ts # API endpoint
├── template.tsx # Re-mounted layout
├── default.tsx # Parallel route fallback
└── opengraph-image.tsx # OG image generationQuick Start
// app/layout.tsx
import { Inter } from 'next/font/google'
import { Providers } from './providers'
const inter = Inter({ subsets: ['latin'] })
export const metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'Built with Next.js App Router',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
)
}
// app/page.tsx - Server Component by default
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // ISR: revalidate every hour
})
return res.json()
}
export default async function HomePage() {
const products = await getProducts()
return (
<main>
<h1>Products</h1>
<ProductGrid products={products} />
</main>
)
}Detailed patterns and worked examples
Detailed pattern documentation lives in references/details.md. Read that file when the navigation tier above is insufficient.
Best Practices
Do's
- Start with Server Components - Add 'use client' only when needed
- Colocate data fetching - Fetch data where it's used
- Use Suspense boundaries - Enable streaming for slow data
- Leverage parallel routes - Independent loading states
- Use Server Actions - For mutations with progressive enhancement
Don'ts
- Don't pass serializable data - Server → Client boundary limitations
- Don't use hooks in Server Components - No useState, useEffect
- Don't fetch in Client Components - Use Server Components or React Query
- Don't over-nest layouts - Each layout adds to the component tree
- Don't ignore loading states - Always provide loading.tsx or Suspense
nextjs-app-router-patterns — detailed patterns and worked examples
Patterns
Pattern 1: Server Components with Data Fetching
// app/products/page.tsx
import { Suspense } from 'react'
import { ProductList, ProductListSkeleton } from '@/components/products'
import { FilterSidebar } from '@/components/filters'
interface SearchParams {
category?: string
sort?: 'price' | 'name' | 'date'
page?: string
}
export default async function ProductsPage({
searchParams,
}: {
searchParams: Promise<SearchParams>
}) {
const params = await searchParams
return (
<div className="flex gap-8">
<FilterSidebar />
<Suspense
key={JSON.stringify(params)}
fallback={<ProductListSkeleton />}
>
<ProductList
category={params.category}
sort={params.sort}
page={Number(params.page) || 1}
/>
</Suspense>
</div>
)
}
// components/products/ProductList.tsx - Server Component
async function getProducts(filters: ProductFilters) {
const res = await fetch(
`${process.env.API_URL}/products?${new URLSearchParams(filters)}`,
{ next: { tags: ['products'] } }
)
if (!res.ok) throw new Error('Failed to fetch products')
return res.json()
}
export async function ProductList({ category, sort, page }: ProductFilters) {
const { products, totalPages } = await getProducts({ category, sort, page })
return (
<div>
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
<Pagination currentPage={page} totalPages={totalPages} />
</div>
)
}Pattern 2: Client Components with 'use client'
// components/products/AddToCartButton.tsx
'use client'
import { useState, useTransition } from 'react'
import { addToCart } from '@/app/actions/cart'
export function AddToCartButton({ productId }: { productId: string }) {
const [isPending, startTransition] = useTransition()
const [error, setError] = useState<string | null>(null)
const handleClick = () => {
setError(null)
startTransition(async () => {
const result = await addToCart(productId)
if (result.error) {
setError(result.error)
}
})
}
return (
<div>
<button
onClick={handleClick}
disabled={isPending}
className="btn-primary"
>
{isPending ? 'Adding...' : 'Add to Cart'}
</button>
{error && <p className="text-red-500 text-sm">{error}</p>}
</div>
)
}Pattern 3: Server Actions
// app/actions/cart.ts
"use server";
import { revalidateTag } from "next/cache";
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export async function addToCart(productId: string) {
const cookieStore = await cookies();
const sessionId = cookieStore.get("session")?.value;
if (!sessionId) {
redirect("/login");
}
try {
await db.cart.upsert({
where: { sessionId_productId: { sessionId, productId } },
update: { quantity: { increment: 1 } },
create: { sessionId, productId, quantity: 1 },
});
revalidateTag("cart");
return { success: true };
} catch (error) {
return { error: "Failed to add item to cart" };
}
}
export async function checkout(formData: FormData) {
const address = formData.get("address") as string;
const payment = formData.get("payment") as string;
// Validate
if (!address || !payment) {
return { error: "Missing required fields" };
}
// Process order
const order = await processOrder({ address, payment });
// Redirect to confirmation
redirect(`/orders/${order.id}/confirmation`);
}Pattern 4: Parallel Routes
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
team,
}: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div className="dashboard-grid">
<main>{children}</main>
<aside className="analytics-panel">{analytics}</aside>
<aside className="team-panel">{team}</aside>
</div>
)
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
const stats = await getAnalytics()
return <AnalyticsChart data={stats} />
}
// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return <ChartSkeleton />
}
// app/dashboard/@team/page.tsx
export default async function TeamSlot() {
const members = await getTeamMembers()
return <TeamList members={members} />
}Pattern 5: Intercepting Routes (Modal Pattern)
// File structure for photo modal
// app/
// ├── @modal/
// │ ├── (.)photos/[id]/page.tsx # Intercept
// │ └── default.tsx
// ├── photos/
// │ └── [id]/page.tsx # Full page
// └── layout.tsx
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/Modal'
import { PhotoDetail } from '@/components/PhotoDetail'
export default async function PhotoModal({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await getPhoto(id)
return (
<Modal>
<PhotoDetail photo={photo} />
</Modal>
)
}
// app/photos/[id]/page.tsx - Full page version
export default async function PhotoPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const photo = await getPhoto(id)
return (
<div className="photo-page">
<PhotoDetail photo={photo} />
<RelatedPhotos photoId={id} />
</div>
)
}
// app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
)
}Pattern 6: Streaming with Suspense
// app/product/[id]/page.tsx
import { Suspense } from 'react'
export default async function ProductPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
// This data loads first (blocking)
const product = await getProduct(id)
return (
<div>
{/* Immediate render */}
<ProductHeader product={product} />
{/* Stream in reviews */}
<Suspense fallback={<ReviewsSkeleton />}>
<Reviews productId={id} />
</Suspense>
{/* Stream in recommendations */}
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations productId={id} />
</Suspense>
</div>
)
}
// These components fetch their own data
async function Reviews({ productId }: { productId: string }) {
const reviews = await getReviews(productId) // Slow API
return <ReviewList reviews={reviews} />
}
async function Recommendations({ productId }: { productId: string }) {
const products = await getRecommendations(productId) // ML-based, slow
return <ProductCarousel products={products} />
}Pattern 7: Route Handlers (API Routes)
// app/api/products/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const category = searchParams.get("category");
const products = await db.product.findMany({
where: category ? { category } : undefined,
take: 20,
});
return NextResponse.json(products);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const product = await db.product.create({
data: body,
});
return NextResponse.json(product, { status: 201 });
}
// app/api/products/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> },
) {
const { id } = await params;
const product = await db.product.findUnique({ where: { id } });
if (!product) {
return NextResponse.json({ error: "Product not found" }, { status: 404 });
}
return NextResponse.json(product);
}Pattern 8: Metadata and SEO
// app/products/[slug]/page.tsx
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
type Props = {
params: Promise<{ slug: string }>
}
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const product = await getProduct(slug)
if (!product) return {}
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [{ url: product.image, width: 1200, height: 630 }],
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.image],
},
}
}
export async function generateStaticParams() {
const products = await db.product.findMany({ select: { slug: true } })
return products.map((p) => ({ slug: p.slug }))
}
export default async function ProductPage({ params }: Props) {
const { slug } = await params
const product = await getProduct(slug)
if (!product) notFound()
return <ProductDetail product={product} />
}Caching Strategies
Data Cache
// No cache (always fresh)
fetch(url, { cache: "no-store" });
// Cache forever (static)
fetch(url, { cache: "force-cache" });
// ISR - revalidate after 60 seconds
fetch(url, { next: { revalidate: 60 } });
// Tag-based invalidation
fetch(url, { next: { tags: ["products"] } });
// Invalidate via Server Action
("use server");
import { revalidateTag, revalidatePath } from "next/cache";
export async function updateProduct(id: string, data: ProductData) {
await db.product.update({ where: { id }, data });
revalidateTag("products");
revalidatePath("/products");
}Related skills
How it compares
Use nextjs-app-router-patterns for file-level App Router composition examples; use Next.js official docs for exhaustive API reference.
FAQ
What patterns does nextjs-app-router-patterns cover?
nextjs-app-router-patterns documents Server Components with data fetching, Suspense streaming with skeleton fallbacks, and searchParams-driven page revalidation. Examples use TypeScript in app/ directory routes such as app/products/page.tsx.
Who should use nextjs-app-router-patterns?
nextjs-app-router-patterns targets developers implementing Next.js App Router frontends who need battle-tested Server Component and Suspense compositions. The skill provides worked code, not deployment or testing configuration.
Is Nextjs App Router Patterns safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.