Back to Skills
    šŸ¦ž

    nextjs-expert

    Use when building Next.js 14/15 applications with the App

    By @jgarrison929
    View on GitHub
    SKILL.md
    ---
    name: nextjs-expert
    version: 1.0.0
    description: Use when building Next.js 14/15 applications with the App Router. Invoke for routing, layouts, Server Components, Client Components, Server Actions, Route Handlers, authentication, middleware, data fetching, caching, revalidation, streaming, Suspense, loading states, error boundaries, dynamic routes, parallel routes, intercepting routes, or any Next.js architecture question.
    triggers:
      - Next.js
      - Next
      - nextjs
      - App Router
      - Server Components
      - Client Components
      - Server Actions
      - use server
      - use client
      - Route Handler
      - middleware
      - layout.tsx
      - page.tsx
      - loading.tsx
      - error.tsx
      - revalidatePath
      - revalidateTag
      - NextAuth
      - Auth.js
      - generateStaticParams
      - generateMetadata
    role: specialist
    scope: implementation
    output-format: code
    ---
    
    # Next.js Expert
    
    Comprehensive Next.js 15 App Router specialist. Adapted from buildwithclaude by Dave Poon (MIT).
    
    ## Role Definition
    
    You are a senior Next.js engineer specializing in the App Router, React Server Components, and production-grade full-stack applications with TypeScript.
    
    ## Core Principles
    
    1. **Server-first**: Components are Server Components by default. Only add `'use client'` when you need hooks, event handlers, or browser APIs.
    2. **Push client boundaries down**: Keep `'use client'` as low in the tree as possible.
    3. **Async params**: In Next.js 15, `params` and `searchParams` are `Promise` types — always `await` them.
    4. **Colocation**: Keep components, tests, and styles near their routes.
    5. **Type everything**: Use TypeScript strictly.
    
    ---
    
    ## App Router File Conventions
    
    ### Route Files
    
    | File | Purpose |
    |------|---------|
    | `page.tsx` | Unique UI for a route, makes it publicly accessible |
    | `layout.tsx` | Shared UI wrapper, preserves state across navigations |
    | `loading.tsx` | Loading UI using React Suspense |
    | `error.tsx` | Error boundary for route segment (must be `'use client'`) |
    | `not-found.tsx` | UI for 404 responses |
    | `template.tsx` | Like layout but re-renders on navigation |
    | `default.tsx` | Fallback for parallel routes |
    | `route.ts` | API endpoint (Route Handler) |
    
    ### Folder Conventions
    
    | Pattern | Purpose | Example |
    |---------|---------|---------|
    | `folder/` | Route segment | `app/blog/` → `/blog` |
    | `[folder]/` | Dynamic segment | `app/blog/[slug]/` → `/blog/:slug` |
    | `[...folder]/` | Catch-all segment | `app/docs/[...slug]/` → `/docs/*` |
    | `[[...folder]]/` | Optional catch-all | `app/shop/[[...slug]]/` → `/shop` or `/shop/*` |
    | `(folder)/` | Route group (no URL) | `app/(marketing)/about/` → `/about` |
    | `@folder/` | Named slot (parallel routes) | `app/@modal/login/` |
    | `_folder/` | Private folder (excluded) | `app/_components/` |
    
    ### File Hierarchy (render order)
    
    1. `layout.tsx` → 2. `template.tsx` → 3. `error.tsx` (boundary) → 4. `loading.tsx` (boundary) → 5. `not-found.tsx` (boundary) → 6. `page.tsx`
    
    ---
    
    ## Pages and Routing
    
    ### Basic Page (Server Component)
    
    ```tsx
    // app/about/page.tsx
    export default function AboutPage() {
      return (
        <main>
          <h1>About Us</h1>
          <p>Welcome to our company.</p>
        </main>
      )
    }
    ```
    
    ### Dynamic Routes
    
    ```tsx
    // app/blog/[slug]/page.tsx
    interface PageProps {
      params: Promise<{ slug: string }>
    }
    
    export default async function BlogPost({ params }: PageProps) {
      const { slug } = await params
      const post = await getPost(slug)
      return <article>{post.content}</article>
    }
    ```
    
    ### Search Params
    
    ```tsx
    // app/search/page.tsx
    interface PageProps {
      searchParams: Promise<{ q?: string; page?: string }>
    }
    
    export default async function SearchPage({ searchParams }: PageProps) {
      const { q, page } = await searchParams
      const results = await search(q, parseInt(page || '1'))
      return <SearchResults results={results} />
    }
    ```
    
    ### Static Generation
    
    ```tsx
    export async function generateStaticParams() {
      const posts = await getAllPosts()
      return posts.map((post) => ({ slug: post.slug }))
    }
    
    // Allow dynamic params not in generateStaticParams
    export const dynamicParams = true
    ```
    
    ---
    
    ## Layouts
    
    ### Root Layout (Required)
    
    ```tsx
    // app/layout.tsx
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>{children}</body>
        </html>
      )
    }
    ```
    
    ### Nested Layout with Data Fetching
    
    ```tsx
    // app/dashboard/layout.tsx
    import { getUser } from '@/lib/get-user'
    
    export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
      const user = await getUser()
      return (
        <div className="flex">
          <Sidebar user={user} />
          <main className="flex-1 p-6">{children}</main>
        </div>
      )
    }
    ```
    
    ### Route Groups for Multiple Root Layouts
    
    ```
    app/
    ā”œā”€ā”€ (marketing)/
    │   ā”œā”€ā”€ layout.tsx          # Marketing layout with <html>/<body>
    │   └── about/page.tsx
    └── (app)/
        ā”œā”€ā”€ layout.tsx          # App layout with <html>/<body>
        └── dashboard/page.tsx
    ```
    
    ### Metadata
    
    ```tsx
    // Static
    export const metadata: Metadata = {
      title: 'About Us',
      description: 'Learn more about our company',
    }
    
    // Dynamic
    export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
      const { slug } = await params
      const post = await getPost(slug)
      return {
        title: post.title,
        openGraph: { title: post.title, images: [post.coverImage] },
      }
    }
    
    // Template in layouts
    export const metadata: Metadata = {
      title: { template: '%s | Dashboard', default: 'Dashboard' },
    }
    ```
    
    ---
    
    ## Server Components vs Client Components
    
    ### Decision Guide
    
    **Server Component (default) when:**
    - Fetching data or accessing backend resources
    - Keeping sensitive info on server (API keys, tokens)
    - Reducing client JavaScript bundle
    - No interactivity needed
    
    **Client Component (`'use client'`) when:**
    - Using `useState`, `useEffect`, `useReducer`
    - Using event handlers (`onClick`, `onChange`)
    - Using browser APIs (`window`, `document`)
    - Using custom hooks with state
    
    ### Composition Patterns
    
    **Pattern 1: Server data → Client interactivity**
    
    ```tsx
    // app/products/page.tsx (Server)
    export default async function ProductsPage() {
      const products = await getProducts()
      return <ProductFilter products={products} />
    }
    
    // components/product-filter.tsx (Client)
    'use client'
    export function ProductFilter({ products }: { products: Product[] }) {
      const [filter, setFilter] = useState('')
      const filtered = products.filter(p => p.name.includes(filter))
      return (
        <>
          <input onChange={e => setFilter(e.target.value)} />
          {filtered.map(p => <ProductCard key={p.id} product={p} />)}
        </>
      )
    }
    ```
    
    **Pattern 2: Children as Server Components**
    
    ```tsx
    // components/client-wrapper.tsx
    'use client'
    export function ClientWrapper({ children }: { children: React.ReactNode }) {
      const [isOpen, setIsOpen] = useState(false)
      return (
        <div>
          <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
          {isOpen && children}
        </div>
      )
    }
    
    // app/page.tsx (Server)
    export default function Page() {
      return (
        <ClientWrapper>
          <ServerContent /> {/* Still renders on server! */}
        </ClientWrapper>
      )
    }
    ```
    
    **Pattern 3: Providers at the boundary**
    
    ```tsx
    // app/providers.tsx
    'use client'
    import { ThemeProvider } from 'next-themes'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    
    const queryClient = new QueryClient()
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <QueryClientProvider client={queryClient}>
          <ThemeProvider attribute="class" defaultTheme="system">
            {children}
          </ThemeProvider>
        </QueryClientProvider>
      )
    }
    ```
    
    ### Shared Data with `cache()`
    
    ```tsx
    import { cache } from 'react'
    
    export const getUser = cache(async () => {
      const response = await fetch('/api/user')
      return response.json()
    })
    
    // Both layout and page call getUser() — only one fetch happens
    ```
    
    ---
    
    ## Data Fetching
    
    ### Async Server Components
    
    ```tsx
    export default async function PostsPage() {
      const posts = await fetch('https://api.example.com/posts').then(r => r.json())
      return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
    }
    ```
    
    ### Parallel Data Fetching
    
    ```tsx
    export default async function DashboardPage() {
      const [user, posts, analytics] = await Promise.all([
        getUser(), getPosts(), getAnalytics()
      ])
      return <Dashboard user={user} posts={posts} analytics={analytics} />
    }
    ```
    
    ### Streaming with Suspense
    
    ```tsx
    import { Suspense } from 'react'
    
    export default function DashboardPage() {
      return (
        <div>
          <h1>Dashboard</h1>
          <Suspense fallback={<StatsSkeleton />}>
            <SlowStats />
          </Suspense>
          <Suspense fallback={<ChartSkeleton />}>
            <SlowChart />
          </Suspense>
        </div>
      )
    }
    ```
    
    ### Caching
    
    ```tsx
    // Cache indefinitely (static)
    const data = await fetch('https://api.example.com/data')
    
    // Revalidate every hour
    const data = await fetch(url, { next: { revalidate: 3600 } })
    
    // No caching (always fresh)
    const data = await fetch(url, { cache: 'no-store' })
    
    // Cache with tags
    const data = await fetch(url, { next: { tags: ['posts'] } })
    ```
    
    ---
    
    ## Loading and Error States
    
    ### Loading UI
    
    ```tsx
    // app/dashboard/loading.tsx
    export default function Loading() {
      return (
        <div className="animate-pulse">
          <div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
          <div className="space-y-3">
            <div className="h-4 bg-gray-200 rounded w-full" />
            <div className="h-4 bg-gray-200 rounded w-5/6" />
          </div>
        </div>
      )
    }
    ```
    
    ### Error Boundary
    
    ```tsx
    // app/dashboard/error.tsx
    'use client'
    
    export default function Error({ error, reset }: { error: Error; reset: () => void }) {
      return (
        <div className="p-4 bg-red-50 border border-red-200 rounded">
          <h2 className="text-red-800 font-bold">Something went wrong!</h2>
          <p className="text-red-600">{error.message}</p>
          <button onClick={reset} className="mt-2 px-4 py-2 
    
    ... (truncated)