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

Next.js Production Engineering

by @1kalin

Build, optimize, and operate production Next.js apps with best practices for architecture, data fetching, caching, rendering, testing, deployment, and observ...

Versionv1.0.0
Downloads949
TERMINAL
clawhub install afrexai-nextjs-production

πŸ“– About This Skill

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

    // βœ… 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 // Pass as props }

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

    ...
    }

    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

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

    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

    // βœ… CORRECT: Parallel fetches
    export default async function DashboardPage() {
      const [user, stats, notifications] = await Promise.all([
        getUser(),
        getStats(),
        getNotifications()
      ])
      return 
    }

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

    import { Suspense } from 'react'

    export default async function Page() { return (

    Dashboard

    {/* Fast: renders immediately */} {/* Slow: streams in when ready */} }> {/* Async Server Component */} }>
    ) }


    Phase 3: Server Actions & Mutations

    Server Action Best Practices

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

    '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 (

    {state?.error?.name &&

    {state.error.name}

    } {state?.success &&

    Saved!

    }
    ) }


    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

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

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

    const rolePermissions: Record> = { 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 }

    Security Headers (next.config.ts)

    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

    import Image from 'next/image'

    // βœ… Always use next/image Hero image

    // For dynamic images {user.name}

    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

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

    import dynamic from 'next/dynamic'

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

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

    Font Optimization

    // 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 ( ${inter.variable} ${jetbrains.variable}}> {children} ) }

    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)

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

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

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

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

    // 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 )
    β”œβ”€β”€ 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

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

    Something went wrong

    {error.digest ? Error ID: ${error.digest} : error.message}

    ) }

    Structured Logging

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

    function log(level: LogLevel, message: string, meta?: Record) { 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) => log('debug', msg, meta), info: (msg: string, meta?: Record) => log('info', msg, meta), warn: (msg: string, meta?: Record) => log('warn', msg, meta), error: (msg: string, meta?: Record) => 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
    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"]

    // next.config.ts β€” required for standalone
    const nextConfig = {
      output: 'standalone'
    }
    

    CI/CD Pipeline (GitHub Actions)

    # .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

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

    '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 ( ) }

    Infinite Scroll

    '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 (

    {items.map(item => )} {hasMore &&
    {isPending ? : null}
    }
    ) }

    Search with URL State

    '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 ( handleSearch(e.target.value)} /> ) }

    Multi-step Form with URL State

    // app/onboarding/page.tsx
    export default function OnboardingPage({
      searchParams
    }: {
      searchParams: { step?: string }
    }) {
      const step = Number(searchParams.step) || 1
      
      return (
        
    {step === 1 && } {step === 2 && } {step === 3 && } {step === 4 && }
    ) }


    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.*