agentleFS
AlteriomAlteriom/ai-dev-skills/skills/nextjs/SKILL.md

nextjs

Skill0 starsChanged 3 months agoMITView on GitHub

Build Next.js 16+ applications with App Router, server components, caching strategies, and production deployment patterns

  • Reads credentials
  • Installs packages

39 other projects use a version of this, 1 of them edited.

Adopted by

Most recently taken up by

Used with

Projects with it also use nuqs, msw and 5 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • thongdn-itSkill

    nuqs

    nuqs (type-safe URL query state) best practices for Next.js applications. This skill should be used when writing, reviewing, or refactoring code that uses nuqs for URL state management. Triggers on tasks involving useQueryState, useQueryStates, search params, URL state, query parameters, nuqs parsers, or Next.js routing with state.

    Also in 3 projects that use this

  • thongdn-itSkill

    msw

    MSW (Mock Service Worker) best practices for API mocking in tests (formerly test-msw). This skill should be used when setting up MSW, writing request handlers, or mocking HTTP APIs. This skill does NOT cover general testing patterns (use test-vitest or test-tdd skills) or test methodology.

    Also in 3 projects that use this

  • VercelSkill

    react

    React renderer for json-render that turns JSON specs into React components. Use when working with @json-render/react, building React UIs from JSON, creating component catalogs, or rendering AI-generated specs.

    Also in 10 projects that use this

  • rawvegSkill

    laravel

    Laravel v12 - The PHP Framework For Web Artisans

    Also in 3 projects that use this

  • MicrosoftSkill

    rust

    Use this guide when working on Rust changes in the FAST monorepo.

    Also in 3 projects that use this

  • +2 more from thongdn-it

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 9 are distinctive, and 2 are very specific to this project.

  • 0

    Common

    Most of the 35 edited versions we read keep these.

  • 9

    Distinctive

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use this skill when:

    line 25
  2. Distinctive:

    Don't use: when you just need client-side React (use Vite), static site generation only (consider Astro), or non-React frameworks.

    line 37
  3. Distinctive:

    Karpathy Principle: Think Before Coding: - Before scaffolding routes, map out your data flow: what needs SSR vs SSG vs client-side?

    line 39
  4. Very specific:

    Karpathy Principle: Simplicity First: - Default to Server Components.

    line 136
  5. Distinctive:

    Before deploying to production:

    line 784
  6. Very specific:

    Use next.config.ts instead of .js

    line 828
  7. Distinctive:

    Use Prisma in Server Components and Server Actions

    line 834
  8. Distinctive:

    Never import Prisma in Client Components

    line 835
  9. Distinctive:

    Use Server Components for layouts/static content

    line 843
  10. Distinctive:

    Use tRPC for type-safe API layer instead of Route Handlers

    line 848
  11. Distinctive:

    All React 18+ features supported (Suspense, Transitions, etc.)

    line 853

“Common” is measured: kept by at least 6 in 10 of the 35 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Next.js - Production-Grade Full-Stack React Framework

When to Use

Use this skill when:

  • Building modern full-stack React applications with server-side rendering
  • Implementing complex routing with nested layouts and parallel routes
  • Optimizing data fetching with Server Components and streaming
  • Setting up production-ready caching strategies (ISR, PPR, static generation)
  • Deploying Next.js apps to Vercel, Docker, or custom Node.js environments
  • Migrating from Pages Router to App Router
  • Implementing authentication flows with middleware and server actions
  • Building API endpoints with Route Handlers
  • Optimizing images, fonts, and static assets for production

Don't use when you just need client-side React (use Vite), static site generation only (consider Astro), or non-React frameworks.

Karpathy Principle: Think Before Coding - Before scaffolding routes, map out your data flow: what needs SSR vs SSG vs client-side? Where are mutations happening? This prevents expensive refactors later.

Prerequisites

Required Knowledge

  • React 18+ fundamentals (components, hooks, props)
  • JavaScript/TypeScript ES6+ (async/await, modules, destructuring)
  • HTTP fundamentals (GET/POST, headers, cookies, status codes)
  • Basic understanding of server vs client rendering

Required Tools

# Node.js 18.17+ or 20+
node --version  # Should be >= 18.17

# Package manager (choose one)
npm --version   # 9+
pnpm --version  # 8+
yarn --version  # 1.22+

# Create new Next.js project
npx create-next-app@latest my-app --typescript --tailwind --app
cd my-app

# Or clone existing project
git clone <repo>
cd <repo>
npm install
npm run dev  # Start dev server on localhost:3000

Project Structure (App Router)

app/
├── layout.tsx          # Root layout (wraps all pages)
├── page.tsx            # Home page (/)
├── loading.tsx         # Loading UI for Suspense
├── error.tsx           # Error boundary
├── not-found.tsx       # 404 page
├── api/
│   └── route.ts        # API Route Handler
├── blog/
│   ├── page.tsx        # /blog
│   └── [slug]/
│       └── page.tsx    # /blog/:slug
public/                 # Static assets
components/             # Reusable components
lib/                    # Utilities, database, etc.

Core Workflows

1. Server vs Client Components

Default: Everything is a Server Component (runs on server, not sent to browser)

// app/page.tsx - Server Component (default)
import { db } from '@/lib/db'

export default async function HomePage() {
  // ✅ Fetch directly in component
  const posts = await db.post.findMany()
  
  return (
    <div>
      <h1>Posts</h1>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  )
}

Client Components: Add 'use client' for interactivity

// components/counter.tsx - Client Component
'use client'

import { useState } from 'react'

export function Counter() {
  // ✅ Can use hooks, event handlers, browser APIs
  const [count, setCount] = useState(0)
  
  return (
    <button onClick={() => setCount(count + 1)}>
      Count: {count}
    </button>
  )
}

Karpathy Principle: Simplicity First - Default to Server Components. Only use 'use client' when you need hooks, event handlers, or browser APIs. Less JavaScript = faster page loads.

Mixing Server and Client:

// app/page.tsx - Server Component
import { ClientForm } from '@/components/client-form'

export default async function Page() {
  const data = await fetchData()  // Server-side data fetching
  
  return (
    <div>
      <h1>Server-rendered heading</h1>
      {/* Pass server data as props to client component */}
      <ClientForm initialData={data} />
    </div>
  )
}

Important Rules:

  • ❌ Can't import Server Component into Client Component directly
  • ✅ Can pass Server Component as children or props to Client Component
  • ❌ Client Components can't be async
  • ✅ Server Components can be async and await data

2. Data Fetching Patterns

Parallel Fetching (Fast):

// ✅ Good - fetches run in parallel
export default async function Page() {
  const [user, posts, comments] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchComments()
  ])
  
  return <Dashboard user={user} posts={posts} comments={comments} />
}

Sequential Fetching (Slow - Avoid):

// ❌ Bad - each fetch waits for previous
export default async function Page() {
  const user = await fetchUser()      // 100ms
  const posts = await fetchPosts()    // waits 100ms, then 150ms
  const comments = await fetchComments() // waits 250ms, then 200ms
  // Total: 450ms instead of 200ms
}

Streaming with Suspense:

// app/page.tsx
import { Suspense } from 'react'
import { Posts } from '@/components/posts'
import { Comments } from '@/components/comments'

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      
      {/* Fast content shows immediately */}
      <UserProfile />
      
      {/* Slow content streams in when ready */}
      <Suspense fallback={<PostsSkeleton />}>
        <Posts />
      </Suspense>
      
      <Suspense fallback={<CommentsSkeleton />}>
        <Comments />
      </Suspense>
    </div>
  )
}

Karpathy Principle: Goal-Driven Execution - Optimize for Time to First Byte and First Contentful Paint. Stream slow content, show fast content immediately.

3. Caching Strategies

Next.js caches aggressively by default. Understand the layers:

Fetch Cache (Default: Cached):

// Cached forever by default
const res = await fetch('https://api.example.com/data')

// Revalidate every 60 seconds (ISR)
const res = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
})

// Never cache (always fresh)
const res = await fetch('https://api.example.com/data', {
  cache: 'no-store'
})

// Revalidate by tag (on-demand)
const res = await fetch('https://api.example.com/data', {
  next: { tags: ['posts'] }
})

Route Segment Cache:

// app/page.tsx

// Static (generated at build time)
export const dynamic = 'force-static'

// Dynamic (rendered on each request)
export const dynamic = 'force-dynamic'

// Revalidate every hour
export const revalidate = 3600

// Export segment config at top of page/layout

Revalidation (Clear Cache):

// app/actions.ts
'use server'

import { revalidatePath, revalidateTag } from 'next/cache'

export async function createPost(data: FormData) {
  await db.post.create({ /* ... */ })
  
  // Revalidate specific path
  revalidatePath('/blog')
  
  // Revalidate by tag (all fetches with this tag)
  revalidateTag('posts')
}

4. Server Actions (Form Mutations)

Basic Server Action:

// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  
  // Validate
  if (!title || !content) {
    return { error: 'Missing fields' }
  }
  
  // Mutate
  await db.post.create({ data: { title, content } })
  
  // Revalidate cache
  revalidatePath('/blog')
  
  // Redirect
  redirect('/blog')
}

Use in Client Component:

// components/post-form.tsx
'use client'

import { createPost } from '@/app/actions'
import { useActionState } from 'react'
import { useFormStatus } from 'react-dom'

export function PostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null)
  
  return (
    <form action={formAction}>
      <input name="title" required />
      <textarea name="content" required />
      <SubmitButton />
      {state?.error && <p className="error">{state.error}</p>}
    </form>
  )
}

function SubmitButton() {
  const { pending } = useFormStatus()
  return (
    <button disabled={pending}>
      {pending ? 'Saving...' : 'Save'}
    </button>
  )
}

Karpathy Principle: Surgical Changes - Server Actions let you mutate data without building API routes. Use them for forms, not as a general API layer.

5. Route Handlers (API Routes)

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'

// GET /api/posts
export async function GET(request: NextRequest) {
  const posts = await db.post.findMany()
  return NextResponse.json(posts)
}

// POST /api/posts
export async function POST(request: NextRequest) {
  const body = await request.json()
  const post = await db.post.create({ data: body })
  return NextResponse.json(post, { status: 201 })
}

// Enable CORS
export async function OPTIONS(request: NextRequest) {
  return new NextResponse(null, {
    status: 200,
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  })
}

Dynamic Route Handler:

// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const post = await db.post.findUnique({
    where: { id: params.id }
  })
  
  if (!post) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 })
  }
  
  return NextResponse.json(post)
}

6. Dynamic Routes and Static Generation

Dynamic Route:

// app/blog/[slug]/page.tsx
interface PageProps {
  params: Promise<{ slug: string }>
  searchParams: { [key: string]: string | string[] | undefined }
}

export default async function BlogPost({ params }: PageProps) {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  })
  
  if (!post) {
    notFound()  // Shows not-found.tsx
  }
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

Static Generation with generateStaticParams:

// app/blog/[slug]/page.tsx

// Generate static pages at build time
export async function generateStaticParams() {
  const posts = await db.post.findMany()
  
  return posts.map(post => ({
    slug: post.slug
  }))
}

// 404 for unknown slugs (default: render on-demand)
export const dynamicParams = false

Metadata (SEO):

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
  const post = await db.post.findUnique({
    where: { slug: params.slug }
  })
  
  if (!post) return { title: 'Not Found' }
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  }
}

7. Middleware (Edge Runtime)

// middleware.ts (root of project)
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  // Check auth
  const token = request.cookies.get('token')
  
  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }
  
  // Add custom header
  const response = NextResponse.next()
  response.headers.set('x-custom-header', 'value')
  
  return response
}

// Run on specific paths only
export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/:path*',
    // Exclude static files
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ]
}

Important: Middleware runs on Edge, not Node.js - no fs, limited npm packages, no direct DB access.

Common Patterns

1. Layout Composition

// app/layout.tsx - Root layout
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Header />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  )
}

// app/dashboard/layout.tsx - Nested layout
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="dashboard">
      <Sidebar />
      <div className="content">{children}</div>
    </div>
  )
}

2. Error Boundaries

// app/error.tsx - Client Component error boundary
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={reset}>Try again</button>
    </div>
  )
}

3. Loading States

// app/loading.tsx - Automatic Suspense boundary
export default function Loading() {
  return <Spinner />
}

// Or use Suspense manually
<Suspense fallback={<Spinner />}>
  <AsyncComponent />
</Suspense>

4. Environment Variables

# .env.local (gitignored - secrets)
DATABASE_URL="postgresql://..."
JWT_SECRET="..."

# .env (committed - public config)
NEXT_PUBLIC_API_URL="https://api.example.com"
// Server Component - access all env vars
const dbUrl = process.env.DATABASE_URL

// Client Component - only NEXT_PUBLIC_* vars
const apiUrl = process.env.NEXT_PUBLIC_API_URL

5. Image Optimization

import Image from 'next/image'

// Local image (width/height inferred from import)
import logo from '@/public/logo.png'

export default function Page() {
  return (
    <>
      {/* Local image */}
      <Image src={logo} alt="Logo" />
      
      {/* Remote image (width/height required) */}
      <Image
        src="https://example.com/photo.jpg"
        alt="Photo"
        width={500}
        height={300}
      />
      
      {/* Fill container (parent must be position: relative) */}
      <div className="relative h-64">
        <Image
          src="/hero.jpg"
          alt="Hero"
          fill
          className="object-cover"
        />
      </div>
    </>
  )
}

Configure allowed domains:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
      },
    ],
  },
}

export default nextConfig

Common Pitfalls

1. ❌ Using Client-Only APIs in Server Components

// ❌ ERROR - window is not defined
export default async function Page() {
  const width = window.innerWidth  // Server Components can't access browser APIs
}

// ✅ CORRECT - extract to Client Component
'use client'
export function ClientComponent() {
  const [width, setWidth] = useState(0)
  
  useEffect(() => {
    setWidth(window.innerWidth)
  }, [])
}

2. ❌ Importing Server Component into Client Component

// ❌ ERROR
'use client'
import { ServerComponent } from './server-component'  // Can't import directly

// ✅ CORRECT - pass as children
export default function Page() {
  return (
    <ClientComponent>
      <ServerComponent />  {/* Pass as children */}
    </ClientComponent>
  )
}

3. ❌ Forgetting Revalidation After Mutations

// ❌ BAD - stale cache after mutation
'use server'
export async function deletePost(id: string) {
  await db.post.delete({ where: { id } })
  // Cache still shows deleted post!
}

// ✅ GOOD - revalidate cache
'use server'
export async function deletePost(id: string) {
  await db.post.delete({ where: { id } })
  revalidatePath('/blog')  // Clear cache
}

Karpathy Principle: Think Before Coding - Map your cache invalidation strategy before building. What paths/tags need revalidation after each mutation?

4. ❌ Over-Fetching in Loops

// ❌ BAD - N+1 query problem
export default async function Page() {
  const users = await db.user.findMany()
  
  return (
    <>
      {users.map(user => (
        <UserCard key={user.id} user={user} />
      ))}
    </>
  )
}

// ❌ UserCard fetches posts for each user (N queries)
async function UserCard({ user }) {
  const posts = await db.post.findMany({ where: { userId: user.id } })
  return <div>{user.name}: {posts.length} posts</div>
}

// ✅ GOOD - fetch all data at once
export default async function Page() {
  const users = await db.user.findMany({
    include: { posts: true }  // Single query with JOIN
  })
  
  return (
    <>
      {users.map(user => (
        <UserCard key={user.id} user={user} posts={user.posts} />
      ))}
    </>
  )
}

5. ❌ Not Handling Loading and Error States

// ❌ BAD - no loading or error UI
export default async function Page() {
  const data = await fetchData()
  return <div>{data.title}</div>
}

// ✅ GOOD - add loading.tsx and error.tsx
// app/loading.tsx
export default function Loading() {
  return <Skeleton />
}

// app/error.tsx
'use client'
export default function Error({ error, reset }) {
  return <ErrorMessage error={error} onRetry={reset} />
}

6. ❌ Excessive Prefetching with <Link>

// ❌ BAD - prefetches all 100 links on page load
{posts.map(post => (
  <Link href={`/blog/${post.slug}`}>{post.title}</Link>
))}

// ✅ GOOD - disable prefetch for long lists
{posts.map(post => (
  <Link href={`/blog/${post.slug}`} prefetch={false}>
    {post.title}
  </Link>
))}

Verification Checklist

Before deploying to production:

Performance

  • Run npm run build - check bundle sizes, no errors
  • Verify Server Components are default (no unnecessary 'use client')
  • Image optimization configured (next/image for all images)
  • Fonts optimized (next/font for Google Fonts or local fonts)
  • Parallel data fetching (no sequential awaits in loops)
  • Streaming with Suspense for slow content

Caching

  • ISR configured for semi-static pages (revalidate: N)
  • On-demand revalidation after mutations (revalidatePath, revalidateTag)
  • Dynamic routes marked correctly (dynamic = 'force-dynamic' where needed)
  • Cache headers correct for Route Handlers

SEO

  • Metadata exported from pages/layouts (generateMetadata)
  • robots.txt and sitemap.xml configured
  • Open Graph images set
  • Canonical URLs for duplicate content

Error Handling

  • error.tsx in each route segment
  • not-found.tsx for 404s
  • Global error boundary (app/global-error.tsx)
  • API error responses (4xx, 5xx with proper messages)

Security

  • Environment variables not exposed to client (no NEXT_PUBLIC_ for secrets)
  • CORS configured for Route Handlers
  • CSP headers set in next.config.ts
  • Authentication middleware protecting routes
  • Input validation in Server Actions

Build

  • No build warnings or errors
  • Output mode correct (standalone for Docker)
  • .env.production configured for production
  • Test build locally: npm run build && npm run start

Integration with Other Skills

With TypeScript

  • Use next.config.ts instead of .js
  • Type params and searchParams in page components
  • Type Server Actions with Zod schema validation

With Prisma

  • Initialize Prisma client in lib/db.ts (singleton pattern)
  • Use Prisma in Server Components and Server Actions
  • Never import Prisma in Client Components

With Zod

  • Validate Server Action inputs with zod
  • Parse formData with zod-form-data
  • Type-safe form errors

With Shadcn/UI

  • Use Server Components for layouts/static content
  • Client Components for interactive UI (forms, dialogs, dropdowns)
  • Combine Server Actions with shadcn forms

With tRPC

  • Use tRPC for type-safe API layer instead of Route Handlers
  • Server Components can call tRPC server-side
  • Client Components use tRPC client

With React

  • All React 18+ features supported (Suspense, Transitions, etc.)
  • Server Components = React Server Components (RSC)
  • Client Components = traditional React components

References

Official Documentation

Key Concepts

Deployment

Best Practices

Meta: Skill Quality

Karpathy Principle: Goal-Driven Execution - This skill prioritizes production-ready patterns over toy examples. Every workflow is designed to scale from prototype to production with minimal refactoring.

Completeness: 9/10 - Covers App Router, Server Components, caching, deployment, but doesn't cover PPR (Partial Prerendering) or advanced Middleware patterns.

Accuracy: 10/10 - Based on Next.js 16+ official docs and real-world production usage.

Practical Examples: 10/10 - All examples are copy-paste ready and follow current best practices.

Maintenance: Last updated April 2026 for Next.js 16+. Review quarterly as Next.js evolves rapidly.

Known Gaps:

  • Advanced middleware patterns (geolocation, A/B testing, rate limiting)
  • Partial Prerendering (PPR) - experimental feature
  • React Server Actions with streaming responses
  • Multi-zone deployments

Related Skills: react-expert, typescript, zod, prisma, shadcn-ui, trpc-best-practices

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.