🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Nextjs Expert

by @jgarrison929

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.

Versionv1.0.0
Downloads10,444
Stars⭐ 12
TERMINAL
clawhub install nextjs-expert

πŸ“– About This Skill


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)

// app/about/page.tsx
export default function AboutPage() {
  return (
    

About Us

Welcome to our company.

) }

Dynamic Routes

// 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

{post.content}
}

Search Params

// 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 }

Static Generation

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)

// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    
      {children}
    
  )
}

Nested Layout with Data Fetching

// app/dashboard/layout.tsx
import { getUser } from '@/lib/get-user'

export default async function DashboardLayout({ children }: { children: React.ReactNode }) { const user = await getUser() return (

{children}
) }

Route Groups for Multiple Root Layouts

app/
β”œβ”€β”€ (marketing)/
β”‚   β”œβ”€β”€ layout.tsx          # Marketing layout with /
β”‚   └── about/page.tsx
└── (app)/
    β”œβ”€β”€ layout.tsx          # App layout with /
    └── dashboard/page.tsx

Metadata

// Static
export const metadata: Metadata = {
  title: 'About Us',
  description: 'Learn more about our company',
}

// Dynamic export async function generateMetadata({ params }: PageProps): Promise { 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

    // app/products/page.tsx (Server)
    export default async function ProductsPage() {
      const products = await getProducts()
      return 
    }

    // 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 ( <> setFilter(e.target.value)} /> {filtered.map(p => )} ) }

    Pattern 2: Children as Server Components

    // components/client-wrapper.tsx
    'use client'
    export function ClientWrapper({ children }: { children: React.ReactNode }) {
      const [isOpen, setIsOpen] = useState(false)
      return (
        
    {isOpen && children}
    ) }

    // app/page.tsx (Server) export default function Page() { return ( {/* Still renders on server! */} ) }

    Pattern 3: Providers at the boundary

    // 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 ( {children} ) }

    Shared Data with cache()

    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

    export default async function PostsPage() {
      const posts = await fetch('https://api.example.com/posts').then(r => r.json())
      return 
      {posts.map(p =>
    • {p.title}
    • )}
    }

    Parallel Data Fetching

    export default async function DashboardPage() {
      const [user, posts, analytics] = await Promise.all([
        getUser(), getPosts(), getAnalytics()
      ])
      return 
    }
    

    Streaming with Suspense

    import { Suspense } from 'react'

    export default function DashboardPage() { return (

    Dashboard

    }> }>
    ) }

    Caching

    // 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

    // app/dashboard/loading.tsx
    export default function Loading() {
      return (
        
    ) }

    Error Boundary

    // app/dashboard/error.tsx
    'use client'

    export default function Error({ error, reset }: { error: Error; reset: () => void }) { return (

    Something went wrong!

    {error.message}

    ) }

    Not Found

    // app/posts/[slug]/page.tsx
    import { notFound } from 'next/navigation'

    export default async function PostPage({ params }: PageProps) { const { slug } = await params const post = await getPost(slug) if (!post) notFound() return

    {post.content}
    }


    Server Actions

    Defining Actions

    // app/actions.ts
    'use server'

    import { z } from 'zod' import { revalidatePath } from 'next/cache' import { redirect } from 'next/navigation'

    const schema = z.object({ title: z.string().min(1).max(200), content: z.string().min(10), })

    export async function createPost(formData: FormData) { const session = await auth() if (!session?.user) throw new Error('Unauthorized')

    const parsed = schema.safeParse({ title: formData.get('title'), content: formData.get('content'), })

    if (!parsed.success) return { error: parsed.error.flatten() }

    const post = await db.post.create({ data: { ...parsed.data, authorId: session.user.id }, })

    revalidatePath('/posts') redirect(/posts/${post.slug}) }

    Form with useFormState and useFormStatus

    // components/submit-button.tsx
    'use client'
    import { useFormStatus } from 'react-dom'

    export function SubmitButton() { const { pending } = useFormStatus() return ( ) }

    // components/create-post-form.tsx 'use client' import { useFormState } from 'react-dom' import { createPost } from '@/app/actions'

    export function CreatePostForm() { const [state, formAction] = useFormState(createPost, {}) return (

    {state.error?.title &&

    {state.error.title[0]}

    }