Next.js Production Engineering

> Complete methodology for building, optimizing, and operating production Next.js applications. From architecture decisions to deployment strategies — everything beyond "hello world."

3,891 stars

Best use case

Next.js Production Engineering is best used when you need a repeatable AI agent workflow instead of a one-off prompt. It is especially useful for teams working in multi. > Complete methodology for building, optimizing, and operating production Next.js applications. From architecture decisions to deployment strategies — everything beyond "hello world."

> Complete methodology for building, optimizing, and operating production Next.js applications. From architecture decisions to deployment strategies — everything beyond "hello world."

Users should expect a more consistent workflow output, faster repeated execution, and less time spent rewriting prompts from scratch.

Practical example

Example input

Use the "Next.js Production Engineering" skill to help with this workflow task. Context: > Complete methodology for building, optimizing, and operating production Next.js applications. From architecture decisions to deployment strategies — everything beyond "hello world."

Example output

A structured workflow result with clearer steps, more consistent formatting, and an output that is easier to reuse in the next run.

When to use this skill

  • Use this skill when you want a reusable workflow rather than writing the same prompt again and again.

When not to use this skill

  • Do not use this when you only need a one-off answer and do not need a reusable workflow.
  • Do not use it if you cannot install or maintain the related files, repository context, or supporting tools.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/afrexai-nextjs-production/SKILL.md --create-dirs "https://raw.githubusercontent.com/openclaw/skills/main/skills/1kalin/afrexai-nextjs-production/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/afrexai-nextjs-production/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How Next.js Production Engineering Compares

Feature / AgentNext.js Production EngineeringStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

> Complete methodology for building, optimizing, and operating production Next.js applications. From architecture decisions to deployment strategies — everything beyond "hello world."

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

Related Guides

SKILL.md Source

# Next.js Production Engineering

> Complete methodology for building, optimizing, and operating production Next.js applications. From architecture decisions to deployment strategies — everything beyond "hello world."

## Quick Health Check (60 seconds)

Run through these 8 signals — score 0 (no) or 2 (yes):

| Signal | Check | Score |
|--------|-------|-------|
| 🏗️ Architecture | Server/Client Component boundary is intentional, not accidental | /2 |
| ⚡ Performance | Core Web Vitals all green (LCP <2.5s, INP <200ms, CLS <0.1) | /2 |
| 🔒 Security | No secrets in client bundles, CSP headers configured | /2 |
| 📦 Bundle | No unnecessary client JS, tree-shaking working | /2 |
| 🗄️ Data | Caching strategy defined (not just defaults) | /2 |
| 🧪 Testing | E2E + unit tests in CI, >70% coverage on critical paths | /2 |
| 🚀 Deploy | Preview deploys, rollback capability, monitoring | /2 |
| 📊 Observability | Error tracking, performance monitoring, structured logging | /2 |

**Score:** /16 → 14-16 Production-ready | 10-13 Needs work | <10 Risk zone

---

## Phase 1: Architecture Decisions

### App Router vs Pages Router Decision

**Default: App Router** for all new projects (Next.js 13.4+).

Use Pages Router ONLY if:
- Migrating existing Pages Router app (incremental adoption)
- Team has zero RSC experience AND shipping deadline <2 weeks
- Library dependency requires Pages Router patterns

### Project Structure (Recommended)

```
src/
├── app/                    # App Router — routes only
│   ├── (auth)/             # Route group — shared auth layout
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── (dashboard)/        # Route group — shared dashboard layout
│   │   ├── layout.tsx
│   │   ├── page.tsx
│   │   └── settings/page.tsx
│   ├── api/                # Route Handlers (use sparingly)
│   │   └── webhooks/
│   │       └── stripe/route.ts
│   ├── layout.tsx          # Root layout
│   ├── loading.tsx         # Root loading
│   ├── error.tsx           # Root error boundary
│   ├── not-found.tsx       # 404 page
│   └── global-error.tsx    # Global error boundary
├── components/             # Shared components
│   ├── ui/                 # Design system primitives
│   ├── forms/              # Form components
│   └── layouts/            # Layout components
├── lib/                    # Shared utilities
│   ├── db/                 # Database client & queries
│   ├── auth/               # Auth utilities
│   ├── api/                # External API clients
│   └── utils/              # Pure utility functions
├── hooks/                  # Custom React hooks (client-only)
├── actions/                # Server Actions
├── types/                  # TypeScript types
├── styles/                 # Global styles
└── config/                 # App configuration
```

### Structure Rules

1. **Routes are thin** — `page.tsx` imports components, doesn't contain business logic
2. **Components are reusable** — never import from `app/` into `components/`
3. **Server Actions get their own directory** — organized by domain, not by page
4. **No barrel files** (`index.ts` re-exports) — they break tree-shaking
5. **Colocation for route-specific** — `_components/` in route folders for non-shared components

### Rendering Strategy Decision Matrix

| Scenario | Strategy | Why |
|----------|----------|-----|
| Static content (blog, docs, marketing) | Static (SSG) | Build-time generation, CDN-cached |
| User-specific dashboard | Dynamic Server | Fresh data per request |
| Product listing with prices | ISR (revalidate: 3600) | Fresh enough, fast delivery |
| Real-time data (chat, stocks) | Client-side + WebSocket | Server can't push updates |
| SEO-critical + fresh data | Dynamic Server + streaming | Fast TTFB with Suspense |
| Highly interactive form/wizard | Client Component | Complex state management |

### Server vs Client Component Rules

```
DEFAULT: Server Component (every .tsx is server by default)

Add "use client" ONLY when you need:
✅ useState, useEffect, useRef, useContext
✅ Browser APIs (window, document, localStorage)
✅ Event handlers (onClick, onChange, onSubmit)
✅ Third-party client libraries (framer-motion, react-hook-form)

NEVER add "use client" because:
❌ You want to use async/await (Server Components support this natively)
❌ You're fetching data (fetch in Server Components, not useEffect)
❌ You're importing a server-only library
❌ "It's not working" — debug the actual issue first
```

### The Boundary Pattern

```tsx
// ✅ CORRECT: Server Component wraps Client Component
// app/dashboard/page.tsx (Server Component)
import { getUser } from '@/lib/auth'
import { DashboardClient } from './_components/dashboard-client'

export default async function DashboardPage() {
  const user = await getUser()  // Server-side data fetch
  return <DashboardClient user={user} />  // Pass as props
}

// _components/dashboard-client.tsx
'use client'
export function DashboardClient({ user }: { user: User }) {
  const [tab, setTab] = useState('overview')
  return <div>...</div>
}
```

**Push "use client" as far down the tree as possible.** The boundary should be at the leaf, not the root.

---

## Phase 2: Data Fetching & Caching

### Data Fetching Hierarchy (Prefer Top → Bottom)

1. **Server Component direct fetch** — simplest, most performant
2. **Server Actions** — for mutations and form submissions
3. **Route Handlers** — for webhooks, external API endpoints
4. **Client-side fetch (SWR/React Query)** — for real-time/polling data only

### Fetch Configuration

```tsx
// Static data (cached indefinitely, revalidated on deploy)
const data = await fetch('https://api.example.com/data', {
  cache: 'force-cache'  // Default in App Router
})

// Revalidate every hour
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 3600 }
})

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

// Tag-based revalidation
const data = await fetch('https://api.example.com/products', {
  next: { tags: ['products'] }
})
// Then in a Server Action:
import { revalidateTag } from 'next/cache'
revalidateTag('products')
```

### Caching Strategy by Data Type

| Data Type | Cache Strategy | Revalidate | Tags |
|-----------|---------------|------------|------|
| CMS content | ISR | 3600s (1h) | `['cms', 'posts']` |
| Product catalog | ISR | 300s (5m) | `['products']` |
| User profile | No cache | — | — |
| Pricing/inventory | No cache | — | — |
| Static assets | Force cache | On deploy | — |
| Analytics/dashboards | ISR | 60s | `['analytics']` |
| Auth tokens | No cache | — | — |

### Database Queries (No fetch API)

```tsx
import { unstable_cache } from 'next/cache'
import { db } from '@/lib/db'

// Cache database queries with tags
const getProducts = unstable_cache(
  async (categoryId: string) => {
    return db.query.products.findMany({
      where: eq(products.categoryId, categoryId)
    })
  },
  ['products'],  // Cache key parts
  {
    revalidate: 300,
    tags: ['products']
  }
)
```

### Parallel Data Fetching

```tsx
// ✅ CORRECT: Parallel fetches
export default async function DashboardPage() {
  const [user, stats, notifications] = await Promise.all([
    getUser(),
    getStats(),
    getNotifications()
  ])
  return <Dashboard user={user} stats={stats} notifications={notifications} />
}

// ❌ WRONG: Sequential waterfall
export default async function DashboardPage() {
  const user = await getUser()
  const stats = await getStats(user.id)  // Waits for user
  const notifications = await getNotifications(user.id)  // Waits for stats
}
```

### Streaming with Suspense

```tsx
import { Suspense } from 'react'

export default async function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Fast: renders immediately */}
      <UserGreeting />
      
      {/* Slow: streams in when ready */}
      <Suspense fallback={<StatsSkeleton />}>
        <StatsPanel />  {/* Async Server Component */}
      </Suspense>
      
      <Suspense fallback={<FeedSkeleton />}>
        <ActivityFeed />
      </Suspense>
    </div>
  )
}
```

---

## Phase 3: Server Actions & Mutations

### Server Action Best Practices

```tsx
// actions/user.ts
'use server'

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

const updateProfileSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  bio: z.string().max(500).optional()
})

export async function updateProfile(formData: FormData) {
  // 1. Authenticate
  const session = await getSession()
  if (!session) throw new Error('Unauthorized')

  // 2. Validate
  const parsed = updateProfileSchema.safeParse({
    name: formData.get('name'),
    email: formData.get('email'),
    bio: formData.get('bio')
  })
  
  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors }
  }

  // 3. Authorize
  if (session.userId !== formData.get('userId')) {
    throw new Error('Forbidden')
  }

  // 4. Mutate
  await db.update(users)
    .set(parsed.data)
    .where(eq(users.id, session.userId))

  // 5. Revalidate
  revalidatePath('/profile')
  
  return { success: true }
}
```

### Server Action Rules

1. **Always validate input** — FormData is user input, never trust it
2. **Always check auth** — Server Actions are public endpoints
3. **Always check authorization** — user can only modify their own data
4. **Use Zod for validation** — type-safe, composable schemas
5. **Return errors, don't throw** — throwing shows error boundary, returning shows inline errors
6. **Revalidate after mutations** — `revalidatePath` or `revalidateTag`
7. **Never return sensitive data** — return only what the client needs

### useActionState Pattern (React 19)

```tsx
'use client'
import { useActionState } from 'react'
import { updateProfile } from '@/actions/user'

export function ProfileForm({ user }: { user: User }) {
  const [state, action, pending] = useActionState(updateProfile, null)

  return (
    <form action={action}>
      <input name="name" defaultValue={user.name} />
      {state?.error?.name && <p className="text-red-500">{state.error.name}</p>}
      
      <button type="submit" disabled={pending}>
        {pending ? 'Saving...' : 'Save'}
      </button>
      
      {state?.success && <p className="text-green-500">Saved!</p>}
    </form>
  )
}
```

---

## Phase 4: Authentication & Authorization

### Auth Pattern Selection

| Method | Best For | Libraries |
|--------|----------|-----------|
| Session-based (cookie) | Traditional web apps | NextAuth.js / Auth.js |
| JWT | API-first, mobile clients | jose, custom |
| OAuth only | Social login, quick start | NextAuth.js |
| Passkeys/WebAuthn | Modern, passwordless | SimpleWebAuthn |
| Third-party | Enterprise, compliance | Clerk, Auth0, Supabase Auth |

### Middleware Auth Pattern

```tsx
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const publicRoutes = ['/', '/login', '/register', '/api/webhooks']
const authRoutes = ['/login', '/register']

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl
  const token = request.cookies.get('session')?.value

  // Public routes — allow
  if (publicRoutes.some(route => pathname.startsWith(route))) {
    // Redirect authenticated users away from auth pages
    if (token && authRoutes.some(route => pathname.startsWith(route))) {
      return NextResponse.redirect(new URL('/dashboard', request.url))
    }
    return NextResponse.next()
  }

  // Protected routes — require auth
  if (!token) {
    const loginUrl = new URL('/login', request.url)
    loginUrl.searchParams.set('callbackUrl', pathname)
    return NextResponse.redirect(loginUrl)
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico|public).*)']
}
```

### Authorization Pattern

```tsx
// lib/auth/permissions.ts
type Permission = 'read' | 'write' | 'admin'
type Resource = 'posts' | 'users' | 'settings'

const rolePermissions: Record<string, Record<Resource, Permission[]>> = {
  admin: {
    posts: ['read', 'write', 'admin'],
    users: ['read', 'write', 'admin'],
    settings: ['read', 'write', 'admin']
  },
  editor: {
    posts: ['read', 'write'],
    users: ['read'],
    settings: ['read']
  },
  viewer: {
    posts: ['read'],
    users: [],
    settings: []
  }
}

export function can(role: string, resource: Resource, permission: Permission): boolean {
  return rolePermissions[role]?.[resource]?.includes(permission) ?? false
}

// Usage in Server Component
export default async function AdminPage() {
  const session = await getSession()
  if (!can(session.role, 'settings', 'admin')) {
    notFound()  // Don't reveal admin pages exist
  }
  return <AdminDashboard />
}
```

### Security Headers (next.config.ts)

```tsx
const securityHeaders = [
  { key: 'X-DNS-Prefetch-Control', value: 'on' },
  { key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' },
  { key: 'X-Frame-Options', value: 'SAMEORIGIN' },
  { key: 'X-Content-Type-Options', value: 'nosniff' },
  { key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
  {
    key: 'Content-Security-Policy',
    value: `
      default-src 'self';
      script-src 'self' 'unsafe-eval' 'unsafe-inline';
      style-src 'self' 'unsafe-inline';
      img-src 'self' data: https:;
      font-src 'self';
      connect-src 'self' https://api.example.com;
      frame-ancestors 'none';
    `.replace(/\n/g, '')
  }
]
```

---

## Phase 5: Performance Optimization

### Core Web Vitals Targets

| Metric | Good | Needs Improvement | Poor |
|--------|------|-------------------|------|
| LCP | <2.5s | 2.5-4.0s | >4.0s |
| INP | <200ms | 200-500ms | >500ms |
| CLS | <0.1 | 0.1-0.25 | >0.25 |
| TTFB | <800ms | 800ms-1.8s | >1.8s |
| FCP | <1.8s | 1.8-3.0s | >3.0s |

### Image Optimization

```tsx
import Image from 'next/image'

// ✅ Always use next/image
<Image
  src="/hero.jpg"
  alt="Hero image"
  width={1200}
  height={630}
  priority  // LCP image — load immediately
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  placeholder="blur"
  blurDataURL={shimmer}  // Base64 placeholder
/>

// For dynamic images
<Image
  src={user.avatar}
  alt={user.name}
  width={48}
  height={48}
  loading="lazy"  // Below fold — lazy load
/>
```

### Image Rules

1. **Always set `priority` on LCP image** (hero, above-fold)
2. **Always provide `sizes`** — prevents downloading oversized images
3. **Use `placeholder="blur"`** for large images — prevents CLS
4. **Configure `remotePatterns`** in next.config.ts for external images
5. **Use WebP/AVIF** — next/image auto-converts by default

### Bundle Optimization

```tsx
// next.config.ts
const nextConfig = {
  // Strict mode for catching bugs
  reactStrictMode: true,
  
  // Optimize packages
  experimental: {
    optimizePackageImports: [
      'lucide-react',
      '@radix-ui/react-icons',
      'date-fns',
      'lodash-es'
    ]
  },
  
  // Bundle analyzer (dev only)
  // npm install @next/bundle-analyzer
  ...(process.env.ANALYZE === 'true' && {
    webpack: (config) => {
      const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
      config.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'static' }))
      return config
    }
  })
}
```

### Dynamic Imports for Heavy Components

```tsx
import dynamic from 'next/dynamic'

// Heavy chart library — only load when needed
const Chart = dynamic(() => import('@/components/chart'), {
  loading: () => <ChartSkeleton />,
  ssr: false  // Client-only component
})

// Code editor — definitely client-only
const CodeEditor = dynamic(() => import('@/components/code-editor'), {
  ssr: false
})
```

### Font Optimization

```tsx
// app/layout.tsx
import { Inter, JetBrains_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter'
})

const jetbrains = JetBrains_Mono({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-mono'
})

export default function RootLayout({ children }) {
  return (
    <html lang="en" className={`${inter.variable} ${jetbrains.variable}`}>
      <body className="font-sans">{children}</body>
    </html>
  )
}
```

### Performance Budget

| Resource | Budget | Tool |
|----------|--------|------|
| First Load JS | <100KB | `next build` output |
| Page JS | <50KB per route | Bundle analyzer |
| Total page weight | <500KB | Lighthouse |
| LCP image | <200KB | next/image handles |
| Third-party scripts | <50KB total | Script component |
| Web fonts | <100KB | next/font handles |

---

## Phase 6: Database & ORM

### ORM Selection Guide

| ORM | Best For | Tradeoffs |
|-----|----------|-----------|
| Drizzle | Type-safe, lightweight, SQL-like | Newer ecosystem |
| Prisma | Rapid prototyping, schema-first | Heavier, edge limitations |
| Kysely | Type-safe raw SQL | More manual, no migrations |
| Raw SQL (pg/mysql2) | Max performance, full control | No type safety, manual migrations |

### Drizzle Setup Pattern (Recommended)

```tsx
// lib/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from './schema'

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20,
  idleTimeoutMillis: 30000
})

export const db = drizzle(pool, { schema })

// lib/db/schema.ts
import { pgTable, text, timestamp, uuid, boolean } from 'drizzle-orm/pg-core'

export const users = pgTable('users', {
  id: uuid('id').defaultRandom().primaryKey(),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  role: text('role', { enum: ['admin', 'editor', 'viewer'] }).default('viewer'),
  emailVerified: boolean('email_verified').default(false),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow()
})
```

### Connection Pooling for Serverless

```tsx
// For Vercel/serverless — use connection pooler
// Neon: use pooler URL (port 5432 → 6543)
// Supabase: use Supavisor URL
// PlanetScale: serverless driver built-in

// lib/db/index.ts (serverless-safe)
import { neon } from '@neondatabase/serverless'
import { drizzle } from 'drizzle-orm/neon-http'

const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql)
```

---

## Phase 7: Testing Strategy

### Test Pyramid for Next.js

| Level | Tool | What to Test | Coverage Target |
|-------|------|-------------|-----------------|
| Unit | Vitest | Utils, hooks, pure functions | 80%+ |
| Component | Testing Library + Vitest | UI components, forms | 70%+ |
| Integration | Testing Library | Page-level with mocked data | Key flows |
| E2E | Playwright | Critical user journeys | 5-10 flows |
| Visual | Playwright screenshots | UI regression | Key pages |

### Vitest Configuration

```tsx
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  plugins: [react(), tsconfigPaths()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    include: ['**/*.test.{ts,tsx}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
      exclude: ['**/*.config.*', '**/types/**']
    }
  }
})
```

### Server Component Testing

```tsx
// Server Components can be tested as async functions
import { render } from '@testing-library/react'
import Page from '@/app/dashboard/page'

// Mock the data fetching
vi.mock('@/lib/db', () => ({
  getUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' })
}))

test('dashboard page renders user name', async () => {
  const Component = await Page()  // Call as async function
  const { getByText } = render(Component)
  expect(getByText('Test')).toBeInTheDocument()
})
```

### Playwright E2E Pattern

```tsx
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'

test.describe('Authentication', () => {
  test('login flow', async ({ page }) => {
    await page.goto('/login')
    await page.fill('[name="email"]', 'test@example.com')
    await page.fill('[name="password"]', 'password123')
    await page.click('button[type="submit"]')
    
    await expect(page).toHaveURL('/dashboard')
    await expect(page.getByText('Welcome')).toBeVisible()
  })
  
  test('protected route redirects', async ({ page }) => {
    await page.goto('/dashboard')
    await expect(page).toHaveURL(/\/login/)
  })
})
```

---

## Phase 8: Error Handling & Monitoring

### Error Boundary Architecture

```
app/
├── global-error.tsx     # Catches root layout errors (must include <html>)
├── error.tsx            # Catches app-level errors
├── not-found.tsx        # 404 page
├── (dashboard)/
│   ├── error.tsx        # Dashboard-specific errors
│   └── settings/
│       └── error.tsx    # Settings-specific errors
```

### Error Component Pattern

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

import { useEffect } from 'react'

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string }
  reset: () => void
}) {
  useEffect(() => {
    // Log to error tracking service
    console.error('Application error:', error)
    // Sentry.captureException(error)
  }, [error])

  return (
    <div className="flex flex-col items-center justify-center min-h-[400px]">
      <h2 className="text-2xl font-bold">Something went wrong</h2>
      <p className="text-gray-500 mt-2">
        {error.digest ? `Error ID: ${error.digest}` : error.message}
      </p>
      <button
        onClick={reset}
        className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        Try again
      </button>
    </div>
  )
}
```

### Structured Logging

```tsx
// lib/logger.ts
type LogLevel = 'debug' | 'info' | 'warn' | 'error'

function log(level: LogLevel, message: string, meta?: Record<string, unknown>) {
  const entry = {
    timestamp: new Date().toISOString(),
    level,
    message,
    ...meta,
    // Add request context if available
    ...(meta?.requestId && { requestId: meta.requestId })
  }
  
  if (level === 'error') {
    console.error(JSON.stringify(entry))
  } else {
    console.log(JSON.stringify(entry))
  }
}

export const logger = {
  debug: (msg: string, meta?: Record<string, unknown>) => log('debug', msg, meta),
  info: (msg: string, meta?: Record<string, unknown>) => log('info', msg, meta),
  warn: (msg: string, meta?: Record<string, unknown>) => log('warn', msg, meta),
  error: (msg: string, meta?: Record<string, unknown>) => log('error', msg, meta)
}
```

---

## Phase 9: Deployment & Infrastructure

### Platform Comparison

| Platform | Best For | Edge | DB | Cost (hobby) |
|----------|----------|------|-----|---------------|
| Vercel | Default choice, best DX | ✅ | External | Free → $20/mo |
| Cloudflare Pages | Edge-first, Workers | ✅ | D1, KV | Free → $5/mo |
| AWS Amplify | AWS ecosystem | ✅ | RDS, DynamoDB | Pay-per-use |
| Railway | Full-stack, Docker | ❌ | Built-in Postgres | $5/mo |
| Fly.io | Global, Docker | ✅ | Built-in Postgres | Pay-per-use |
| Self-hosted (Docker) | Full control | ❌ | Any | Server cost |

### Docker Production Setup

```dockerfile
# Dockerfile
FROM node:20-alpine AS base
RUN corepack enable

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]
```

```tsx
// next.config.ts — required for standalone
const nextConfig = {
  output: 'standalone'
}
```

### CI/CD Pipeline (GitHub Actions)

```yaml
# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm tsc --noEmit
      - run: pnpm lint
      - run: pnpm test -- --coverage
      - run: pnpm build
      
  e2e:
    runs-on: ubuntu-latest
    needs: quality
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec playwright install --with-deps
      - run: pnpm build
      - run: pnpm exec playwright test
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
```

### Environment Variables

```tsx
// env.ts — runtime validation with t3-env
import { createEnv } from '@t3-oss/env-nextjs'
import { z } from 'zod'

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    AUTH_SECRET: z.string().min(32),
    STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
    REDIS_URL: z.string().url().optional(),
  },
  client: {
    NEXT_PUBLIC_APP_URL: z.string().url(),
    NEXT_PUBLIC_STRIPE_KEY: z.string().startsWith('pk_'),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    AUTH_SECRET: process.env.AUTH_SECRET,
    STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY,
    REDIS_URL: process.env.REDIS_URL,
    NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
    NEXT_PUBLIC_STRIPE_KEY: process.env.NEXT_PUBLIC_STRIPE_KEY,
  },
})
```

---

## Phase 10: Common Patterns Library

### Optimistic Updates

```tsx
'use client'
import { useOptimistic, useTransition } from 'react'
import { toggleTodo } from '@/actions/todos'

export function TodoItem({ todo }: { todo: Todo }) {
  const [optimisticTodo, setOptimisticTodo] = useOptimistic(todo)
  const [, startTransition] = useTransition()

  return (
    <label>
      <input
        type="checkbox"
        checked={optimisticTodo.completed}
        onChange={() => {
          startTransition(async () => {
            setOptimisticTodo({ ...todo, completed: !todo.completed })
            await toggleTodo(todo.id)
          })
        }}
      />
      {optimisticTodo.title}
    </label>
  )
}
```

### Infinite Scroll

```tsx
'use client'
import { useInView } from 'react-intersection-observer'
import { useEffect, useState, useTransition } from 'react'
import { loadMore } from '@/actions/feed'

export function InfiniteList({ initialItems }: { initialItems: Item[] }) {
  const [items, setItems] = useState(initialItems)
  const [cursor, setCursor] = useState(initialItems.at(-1)?.id)
  const [hasMore, setHasMore] = useState(true)
  const [isPending, startTransition] = useTransition()
  const { ref, inView } = useInView()

  useEffect(() => {
    if (inView && hasMore && !isPending) {
      startTransition(async () => {
        const newItems = await loadMore(cursor)
        if (newItems.length === 0) {
          setHasMore(false)
        } else {
          setItems(prev => [...prev, ...newItems])
          setCursor(newItems.at(-1)?.id)
        }
      })
    }
  }, [inView, hasMore, isPending, cursor])

  return (
    <div>
      {items.map(item => <ItemCard key={item.id} item={item} />)}
      {hasMore && <div ref={ref}>{isPending ? <Spinner /> : null}</div>}
    </div>
  )
}
```

### Search with URL State

```tsx
'use client'
import { useRouter, useSearchParams, usePathname } from 'next/navigation'
import { useDebouncedCallback } from 'use-debounce'

export function SearchBar() {
  const router = useRouter()
  const pathname = usePathname()
  const searchParams = useSearchParams()

  const handleSearch = useDebouncedCallback((term: string) => {
    const params = new URLSearchParams(searchParams)
    if (term) {
      params.set('q', term)
      params.set('page', '1')
    } else {
      params.delete('q')
    }
    router.replace(`${pathname}?${params.toString()}`)
  }, 300)

  return (
    <input
      type="search"
      placeholder="Search..."
      defaultValue={searchParams.get('q') ?? ''}
      onChange={e => handleSearch(e.target.value)}
    />
  )
}
```

### Multi-step Form with URL State

```tsx
// app/onboarding/page.tsx
export default function OnboardingPage({
  searchParams
}: {
  searchParams: { step?: string }
}) {
  const step = Number(searchParams.step) || 1
  
  return (
    <div>
      <ProgressBar step={step} total={4} />
      {step === 1 && <StepOne />}
      {step === 2 && <StepTwo />}
      {step === 3 && <StepThree />}
      {step === 4 && <StepFour />}
    </div>
  )
}
```

---

## Phase 11: Production Checklist

### Pre-Launch (Mandatory)

- [ ] `next build` succeeds with zero warnings
- [ ] TypeScript strict mode, no `any` types in production code
- [ ] All environment variables validated (t3-env or manual)
- [ ] Security headers configured (CSP, HSTS, X-Frame-Options)
- [ ] Authentication + authorization tested (pen test critical flows)
- [ ] Error boundaries at every route level
- [ ] 404 and 500 pages customized
- [ ] Favicon, OG images, meta tags configured
- [ ] Core Web Vitals passing (Lighthouse >90)
- [ ] Mobile responsive tested on real devices
- [ ] Accessibility audit (axe, keyboard nav, screen reader)
- [ ] Rate limiting on API routes and Server Actions
- [ ] CORS configured correctly
- [ ] Database connection pooling configured for serverless
- [ ] Monitoring & error tracking connected (Sentry, etc.)

### Pre-Launch (Recommended)

- [ ] E2E tests for critical user journeys
- [ ] Bundle size within budget (<100KB first load)
- [ ] Image optimization verified (next/image, proper sizes)
- [ ] Sitemap.xml and robots.txt configured
- [ ] Analytics configured
- [ ] Preview deployment tested
- [ ] Rollback plan documented
- [ ] Load testing completed
- [ ] CDN caching verified
- [ ] Edge middleware tested in production environment

---

## Phase 12: Anti-Patterns & Troubleshooting

### 10 Next.js Mistakes

| # | Mistake | Fix |
|---|---------|-----|
| 1 | `"use client"` at the top of every file | Default to Server Components, push client boundary down |
| 2 | Fetching data with useEffect | Fetch in Server Components or use SWR/React Query for client |
| 3 | Not using `loading.tsx` | Add loading states to prevent layout shift |
| 4 | Ignoring bundle size | Run `next build` and check output, use dynamic imports |
| 5 | No error boundaries | Add `error.tsx` at every route level |
| 6 | Storing secrets in `NEXT_PUBLIC_*` | Server-only env vars for secrets, validate with t3-env |
| 7 | Not setting image `sizes` prop | Always provide `sizes` for responsive images |
| 8 | Sequential data fetching | Use `Promise.all()` for parallel fetches |
| 9 | Caching everything or nothing | Explicit cache strategy per data type |
| 10 | Not using `revalidateTag` | Tag-based revalidation for precise cache control |

### Troubleshooting Decision Tree

```
Build error?
├── "Module not found" → Check import paths, tsconfig paths
├── "Server Component error" → Remove "use client" or move hooks to client component
├── "Hydration mismatch" → Check for browser-only code in shared components
│   → Use suppressHydrationWarning for timestamps
│   → Wrap in useEffect or dynamic(ssr: false)
├── "Edge runtime error" → Check node APIs (fs, crypto) not available at edge
└── Slow build → Check for large static generation, reduce ISR pages

Runtime error?
├── 500 on production → Check error.tsx, logs, Sentry
├── Slow TTFB → Check database queries, add caching
├── CLS → Add explicit dimensions to images/embeds
├── High JS bundle → Run bundle analyzer, dynamic import heavy libs
└── Stale data → Check revalidation settings, revalidateTag
```

---

## Recommended Stack (2025+)

| Layer | Recommendation | Why |
|-------|---------------|-----|
| Framework | Next.js 15+ (App Router) | RSC, streaming, Server Actions |
| Language | TypeScript (strict) | Type safety, better DX |
| Styling | Tailwind CSS 4 | Utility-first, no runtime cost |
| UI Components | shadcn/ui | Copy-paste, customizable |
| Forms | react-hook-form + zod | Type-safe validation |
| ORM | Drizzle | Type-safe, lightweight, SQL-like |
| Database | PostgreSQL (Neon/Supabase) | Serverless-friendly, proven |
| Auth | Auth.js (NextAuth v5) | Built for Next.js |
| Payments | Stripe | Industry standard |
| Hosting | Vercel | Best Next.js DX |
| Testing | Vitest + Playwright | Fast unit + reliable E2E |
| Monitoring | Sentry | Error tracking + performance |
| Analytics | PostHog | Product analytics, open source |

---

## Quality Rubric (0-100)

| Dimension | Weight | Scoring |
|-----------|--------|---------|
| Architecture (RSC boundaries, structure) | 20% | 0-20 |
| Performance (CWV, bundle, TTFB) | 20% | 0-20 |
| Security (auth, headers, validation) | 15% | 0-15 |
| Data layer (caching, fetching, DB) | 15% | 0-15 |
| Testing (pyramid, coverage, E2E) | 10% | 0-10 |
| Error handling (boundaries, logging) | 10% | 0-10 |
| DX (types, linting, CI) | 5% | 0-5 |
| Deployment (Docker/platform, monitoring) | 5% | 0-5 |

**Score:** 90-100 Elite | 75-89 Production-ready | 60-74 Needs improvement | <60 Not production-ready

---

## Natural Language Commands

1. "Set up a new Next.js project" → Phase 1 architecture + structure + Phase 6 DB setup
2. "Add authentication" → Phase 4 auth pattern + middleware + authorization
3. "Optimize performance" → Phase 5 full checklist + image + bundle + fonts
4. "Set up testing" → Phase 7 full pyramid + Vitest + Playwright config
5. "Deploy to production" → Phase 9 platform selection + Docker + CI/CD + env vars
6. "Fix hydration error" → Phase 12 troubleshooting tree
7. "Add caching" → Phase 2 caching strategy table + fetch config + tags
8. "Create a Server Action" → Phase 3 best practices + useActionState pattern
9. "Audit my app" → Quick health check + Phase 11 production checklist
10. "Add error handling" → Phase 8 error boundary architecture + logging
11. "Set up search" → Phase 10 search with URL state pattern
12. "Review my architecture" → Phase 1 decision matrix + rendering strategy

---

*Built by AfrexAI — the AI automation agency that ships. Zero dependencies.*

Related Skills

afrexai-performance-engineering

3891
from openclaw/skills

Complete performance engineering system — profiling, optimization, load testing, capacity planning, and performance culture. Use when diagnosing slow applications, optimizing code/queries/infrastructure, load testing before launch, planning capacity, or building performance into CI/CD. Covers Node.js, Python, Go, Java, databases, APIs, and frontend.

DevOps & Infrastructure

OpenClaw Mastery — The Complete Agent Engineering & Operations System

3891
from openclaw/skills

> Built by AfrexAI — the team that runs 9+ production agents 24/7 on OpenClaw.

DevOps & Infrastructure

n8n Workflow Mastery — Complete Automation Engineering System

3891
from openclaw/skills

You are an expert n8n workflow architect. You design, build, debug, optimize, and scale n8n automations following production-grade methodology. Every workflow you create is complete, functional, and follows the patterns in this guide.

Workflow & Productivity

ML & AI Engineering System

3891
from openclaw/skills

Complete methodology for building, deploying, and operating production ML/AI systems — from experiment to scale.

MCP Engineering — Complete Model Context Protocol System

3891
from openclaw/skills

Build, integrate, secure, and scale MCP servers and clients. From first server to production multi-tool architecture.

AI Infrastructure & Integrations

Growth Engineering Mastery

3891
from openclaw/skills

> Complete growth system: experimentation engine, viral mechanics, channel playbooks, funnel optimization, retention loops, and scaling frameworks. From zero users to exponential growth.

Go Production Engineering

3891
from openclaw/skills

You are a Go production engineering expert. Follow this system for every Go project — from architecture decisions through production deployment. Apply phases sequentially for new projects; use individual phases as needed for existing codebases.

Coding & Development

Git Engineering & Repository Strategy

3891
from openclaw/skills

You are a Git Engineering expert. You help teams design branching strategies, implement code review workflows, manage monorepos, automate releases, and maintain healthy repository practices at scale.

DevOps & Infrastructure

Fitness & Training Engineering

3891
from openclaw/skills

> Complete periodized training system — program design, progressive overload, recovery optimization, body composition, and race prep. Works for any goal: strength, hypertrophy, endurance, hybrid (Hyrox/CrossFit), or general fitness. Zero dependencies.

Health & Fitness

FastAPI Production Engineering

3891
from openclaw/skills

Complete methodology for building, deploying, and scaling production FastAPI applications. Not a tutorial — a production operating system.

Engineering Manager OS

3891
from openclaw/skills

Complete engineering management system — team building, 1:1s, performance, hiring, architecture decisions, incident management, and scaling. From IC-to-manager transition through director-level operations.

Workflow & Productivity

Django Production Engineering

3891
from openclaw/skills

Complete methodology for building, scaling, and operating production Django applications. From project structure to deployment, security to performance — every decision framework a Django team needs.

DevOps & Infrastructure