← Back to Skills Marketplace
1kalin

Next.js Production Engineering

by 1kalin · GitHub ↗ · v1.0.0
cross-platform ✓ Security Clean
525
Downloads
0
Stars
0
Active Installs
1
Versions
Install in OpenClaw
/install afrexai-nextjs-production
Description
Build, optimize, and operate production Next.js apps with best practices for architecture, data fetching, caching, rendering, testing, deployment, and observ...
README (SKILL.md)

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 \x3C2.5s, INP \x3C200ms, CLS \x3C0.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 | \x3C10 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 \x3C2 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 thinpage.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 \x3CDashboardClient user={user} />  // Pass as props
}

// _components/dashboard-client.tsx
'use client'
export function DashboardClient({ user }: { user: User }) {
  const [tab, setTab] = useState('overview')
  return \x3Cdiv>...\x3C/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

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

import { Suspense } from 'react'

export default async function Page() {
  return (
    \x3Cdiv>
      \x3Ch1>Dashboard\x3C/h1>
      {/* Fast: renders immediately */}
      \x3CUserGreeting />
      
      {/* Slow: streams in when ready */}
      \x3CSuspense fallback={\x3CStatsSkeleton />}>
        \x3CStatsPanel />  {/* Async Server Component */}
      \x3C/Suspense>
      
      \x3CSuspense fallback={\x3CFeedSkeleton />}>
        \x3CActivityFeed />
      \x3C/Suspense>
    \x3C/div>
  )
}

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 mutationsrevalidatePath 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 (
    \x3Cform action={action}>
      \x3Cinput name="name" defaultValue={user.name} />
      {state?.error?.name && \x3Cp className="text-red-500">{state.error.name}\x3C/p>}
      
      \x3Cbutton type="submit" disabled={pending}>
        {pending ? 'Saving...' : 'Save'}
      \x3C/button>
      
      {state?.success && \x3Cp className="text-green-500">Saved!\x3C/p>}
    \x3C/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

// 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\x3Cstring, Record\x3CResource, 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 \x3CAdminDashboard />
}

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(/\
/g, '')
  }
]

Phase 5: Performance Optimization

Core Web Vitals Targets

Metric Good Needs Improvement Poor
LCP \x3C2.5s 2.5-4.0s >4.0s
INP \x3C200ms 200-500ms >500ms
CLS \x3C0.1 0.1-0.25 >0.25
TTFB \x3C800ms 800ms-1.8s >1.8s
FCP \x3C1.8s 1.8-3.0s >3.0s

Image Optimization

import Image from 'next/image'

// ✅ Always use next/image
\x3CImage
  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
\x3CImage
  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

// 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: () => \x3CChartSkeleton />,
  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 (
    \x3Chtml lang="en" className={`${inter.variable} ${jetbrains.variable}`}>
      \x3Cbody className="font-sans">{children}\x3C/body>
    \x3C/html>
  )
}

Performance Budget

Resource Budget Tool
First Load JS \x3C100KB next build output
Page JS \x3C50KB per route Bundle analyzer
Total page weight \x3C500KB Lighthouse
LCP image \x3C200KB next/image handles
Third-party scripts \x3C50KB total Script component
Web fonts \x3C100KB 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"]', '[email protected]')
    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 \x3Chtml>)
├── 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 (
    \x3Cdiv className="flex flex-col items-center justify-center min-h-[400px]">
      \x3Ch2 className="text-2xl font-bold">Something went wrong\x3C/h2>
      \x3Cp className="text-gray-500 mt-2">
        {error.digest ? `Error ID: ${error.digest}` : error.message}
      \x3C/p>
      \x3Cbutton
        onClick={reset}
        className="mt-4 px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
      >
        Try again
      \x3C/button>
    \x3C/div>
  )
}

Structured Logging

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

function log(level: LogLevel, message: string, meta?: Record\x3Cstring, 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\x3Cstring, unknown>) => log('debug', msg, meta),
  info: (msg: string, meta?: Record\x3Cstring, unknown>) => log('info', msg, meta),
  warn: (msg: string, meta?: Record\x3Cstring, unknown>) => log('warn', msg, meta),
  error: (msg: string, meta?: Record\x3Cstring, 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
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 (
    \x3Clabel>
      \x3Cinput
        type="checkbox"
        checked={optimisticTodo.completed}
        onChange={() => {
          startTransition(async () => {
            setOptimisticTodo({ ...todo, completed: !todo.completed })
            await toggleTodo(todo.id)
          })
        }}
      />
      {optimisticTodo.title}
    \x3C/label>
  )
}

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 (
    \x3Cdiv>
      {items.map(item => \x3CItemCard key={item.id} item={item} />)}
      {hasMore && \x3Cdiv ref={ref}>{isPending ? \x3CSpinner /> : null}\x3C/div>}
    \x3C/div>
  )
}

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 (
    \x3Cinput
      type="search"
      placeholder="Search..."
      defaultValue={searchParams.get('q') ?? ''}
      onChange={e => 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 (
    \x3Cdiv>
      \x3CProgressBar step={step} total={4} />
      {step === 1 && \x3CStepOne />}
      {step === 2 && \x3CStepTwo />}
      {step === 3 && \x3CStepThree />}
      {step === 4 && \x3CStepFour />}
    \x3C/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 (\x3C100KB 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 | \x3C60 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.

Usage Guidance
This appears to be a coherent, instruction-only Next.js production guide. Because it is just prose, the main risk is that your agent might propose shell commands, deployment steps, or CI configurations derived from the guide — always review any commands before running them, do not paste secrets or private keys into prompts, and test suggested changes in a safe/dev environment first. If you need extra assurance, check the publisher links (the README references afrexai-cto.github.io) and prefer skills with a verifiable homepage or repository if you plan to follow copy-paste code into production.
Capability Analysis
Type: OpenClaw Skill Name: afrexai-nextjs-production Version: 1.0.0 The skill bundle provides a comprehensive guide for Next.js production engineering, covering architecture, performance, security, testing, and deployment. All code snippets and instructions promote best practices, including secure Docker configurations, CI/CD pipelines, environment variable validation, and security headers. There is no evidence of intentional harmful behavior, data exfiltration, unauthorized execution commands, prompt injection with malicious objectives, or obfuscation. The external links are for marketing other skills/products by the same owner, which is not malicious.
Capability Assessment
Purpose & Capability
Name/description align with the content: the skill is a methodology guide for Next.js production apps and it does not request unrelated binaries, credentials, or install actions.
Instruction Scope
SKILL.md is a large, prescriptive set of best-practice instructions and patterns; it does not appear to instruct the agent to read unrelated system files or exfiltrate secrets. The guidance focuses on code patterns, architecture, caching, testing, and deployment.
Install Mechanism
No install spec and no code files — instruction-only — so nothing will be downloaded or written to disk by the skill itself.
Credentials
The skill declares no required environment variables, credentials, or config paths. The content discusses deployment and CI/CD patterns in general but does not demand unrelated credentials.
Persistence & Privilege
always is false and the skill is user-invocable; it does not request persistent presence or attempt to modify other skills or global agent settings.
How to Use
  1. Make sure OpenClaw is installed (local or Docker)
  2. Run the install command in chat: /install afrexai-nextjs-production
  3. After installation, invoke the skill by name or use /afrexai-nextjs-production
  4. Provide required inputs per the skill's parameter spec and get structured output
Version History
v1.0.0
afrexai-nextjs-production 1.0.0 - Initial release of the skill, providing a complete methodology for building, optimizing, and operating production-grade Next.js applications. - Includes a quick 8-point production health check for rapid project assessment. - Details architecture decisions: clear guidance on App Router vs. Pages Router, recommended project structure, server/client boundaries, and rendering strategies. - Outlines data fetching and caching best practices, including fetch configurations, cache strategies by data type, and parallelization techniques. - Introduces patterns for maximizing code efficiency, maintainability, performance, and security in production Next.js projects.
Metadata
Slug afrexai-nextjs-production
Version 1.0.0
License
All-time Installs 0
Active Installs 0
Total Versions 1
Frequently Asked Questions

What is Next.js Production Engineering?

Build, optimize, and operate production Next.js apps with best practices for architecture, data fetching, caching, rendering, testing, deployment, and observ... It is an AI Agent Skill for Claude Code / OpenClaw, with 525 downloads so far.

How do I install Next.js Production Engineering?

Run "/install afrexai-nextjs-production" in the OpenClaw or Claude Code chat to install it in one step — no extra setup required.

Is Next.js Production Engineering free?

Yes, Next.js Production Engineering is completely free (open-source). You can download, install and use it at no cost.

Which platforms does Next.js Production Engineering support?

Next.js Production Engineering is cross-platform and runs anywhere OpenClaw / Claude Code is available (cross-platform).

Who created Next.js Production Engineering?

It is built and maintained by 1kalin (@1kalin); the current version is v1.0.0.

💬 Comments