
Nextjs App Router Patterns
Apply proven App Router layouts for server components, Suspense boundaries, and typed searchParams when scaffolding or extending a Next.js product.
Overview
Next.js App Router Patterns is an agent skill for the Build phase that documents Server Component layouts, Suspense streaming, and searchParams-driven list pages for Next.js.
Install
npx skills add https://github.com/wshobson/agents --skill nextjs-app-router-patternsWhat is this skill?
- Server Components page pattern with async searchParams (Promise-based) and typed filters
- Suspense keyed to searchParams so lists refetch without full-page reload
- Tagged fetch with next: { tags } for cache invalidation on product lists
- Split layout shell vs. async ProductList server component with skeleton fallback
- Worked examples for grid listing pages and filter sidebars
Adoption & trust: 19.5k installs on skills.sh; 36.5k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You are on the App Router but your agent keeps generating pages-router habits, missing Suspense boundaries, or fetching in the wrong component layer.
Who is it for?
Solo builders extending an existing Next.js App Router SaaS or marketing site who want opinionated list-page and RSC data-fetch patterns.
Skip if: Greenfield framework comparison, Pages Router migrations only, or teams that need full auth, payments, and deployment in one skill.
When should I use this skill?
Implementing or refactoring Next.js App Router pages with Server Components, Suspense, and URL-driven filters.
What do I get? / Deliverables
You get consistent page.tsx and server-component files with typed searchParams, skeleton fallbacks, and tagged fetches ready to drop into your repo.
- page.tsx server route patterns
- async server component modules with fetch and error handling
Recommended Skills
Journey fit
Canonical shelf is Build because the skill encodes implementation patterns for the Next.js App Router, which is core product UI and data-loading architecture. Frontend is the right subphase: Server Components, page.tsx structure, Suspense fallbacks, and client/server split are all UI-route concerns.
How it compares
Use as a pattern cookbook alongside generic React skills—not as a substitute for Next.js official docs or a full-stack scaffold.
Common Questions / FAQ
Who is nextjs-app-router-patterns for?
Indie and solo developers using Claude Code, Cursor, or Codex to build or refactor Next.js App Router pages, especially product listings and filtered views.
When should I use nextjs-app-router-patterns?
During Build when you are implementing routes, server components, and loading states; also when Ship review finds inconsistent fetch or Suspense usage on marketing or dashboard pages.
Is nextjs-app-router-patterns safe to install?
Treat it as documentation-style procedural knowledge in your agent bundle; review the Security Audits panel on this Prism page before enabling in CI or shared workspaces.
SKILL.md
READMESKILL.md - Nextjs App Router Patterns
# nextjs-app-router-patterns — detailed patterns and worked examples ## Patterns ### Pattern 1: Server Components with Data Fetching ```typescript // 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' ```typescript // 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 ```typescript // 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 ```typescript // app/dashboard/layout.tsx export default function DashboardLayout({ children, analytics, team, }: { children: React.ReactNode analytics: React.ReactNode team: React.ReactN