Contents

Chapter 1

Architecture Guide

This document explains the architectural decisions behind the Next.js Starter Kit — why things are structured the way they are, and how to extend the architecture as your app grows.

Directory Structure

├── app/                      # Next.js App Router pages and layouts
│   ├── api/                  # API route handlers (serverless functions)
│   │   ├── auth/route.ts     # Authentication endpoints
│   │   ├── posts/route.ts    # Blog/content CRUD
│   │   └── users/route.ts    # User management
│   ├── blog/                 # Public blog with ISR
│   │   ├── page.tsx          # Blog listing (ISR, 60s revalidation)
│   │   └── [slug]/page.tsx   # Individual post (ISR + generateStaticParams)
│   ├── dashboard/            # Protected area
│   │   ├── layout.tsx        # Dashboard shell (sidebar, nav)
│   │   └── page.tsx          # Dashboard home
│   ├── layout.tsx            # Root layout (HTML shell, global providers)
│   ├── page.tsx              # Landing page (Server Component)
│   ├── loading.tsx           # Global loading skeleton
│   ├── error.tsx             # Global error boundary
│   └── not-found.tsx         # 404 page
├── components/               # Shared React components
│   ├── Header.tsx            # Site header with auth-aware navigation
│   ├── Footer.tsx            # Site footer with link columns
│   └── PostCard.tsx          # Blog post card (3 variants)
├── src/
│   ├── lib/                  # Core libraries and utilities
│   │   ├── auth.ts           # JWT auth, password hashing, session management
│   │   ├── prisma.ts         # Prisma client singleton
│   │   └── utils.ts          # Shared utilities (slugify, formatting, etc.)
│   └── types/                # TypeScript type definitions
│       └── index.ts          # Shared types (User, Post, ApiResponse, etc.)
├── prisma/
│   └── schema.prisma         # Database schema
├── middleware.ts              # Edge middleware for route protection
├── next.config.ts             # Next.js configuration
├── vercel.json                # Vercel deployment configuration
└── package.json               # Dependencies and scripts

Key Architecture Decisions

1. App Router Over Pages Router

We use the App Router (introduced in Next.js 13, stable in 14) instead of the legacy Pages Router. Here's why:

FeaturePages RouterApp Router (this starter)
Rendering modelPages default to client-sideComponents default to server-side
Data fetchinggetServerSideProps, getStaticPropsasync Server Components, fetch() with caching
LayoutsCustom _app.tsx wrapperNested layout.tsx files (preserved across navigation)
Loading statesManual implementationBuilt-in loading.tsx per route segment
Error handlingCustom _error.tsxBuilt-in error.tsx per route segment
MetadataManual managementmetadata export or generateMetadata()
StreamingNot supportedBuilt-in with

The App Router is the future of Next.js. All new features ship for App Router first.

2. Server Components by Default

Every component in this starter is a Server Component unless explicitly marked with "use client". This matters for three reasons:

1. Bundle size — Server Components send zero JavaScript to the client. The PostCard, Footer, and most of the app/ pages ship as pure HTML.

2. Data access — Server Components can directly query the database, read files, or call APIs without exposing credentials to the client. No need for an API route as a middleman.

3. Performance — Server Components stream HTML progressively. The user sees content faster because the browser doesn't need to download, parse, and execute JavaScript before rendering.

When to use "use client":

  • Event handlers (onClick, onChange, onSubmit)
  • Browser APIs (useState, useEffect, useRef, usePathname)
  • Third-party libraries that use browser APIs

In this starter, only Header.tsx needs "use client" (for the mobile menu toggle and active link detection).

3. JWT Authentication (Not NextAuth)

We implement authentication from scratch using JWTs instead of using NextAuth.js. This is deliberate:

Why not NextAuth?

  • NextAuth abstracts away the auth flow, making it harder to understand and debug
  • For a SaaS app, you often need custom auth logic (API keys, team invitations, RBAC)
  • NextAuth's session strategy adds complexity that isn't needed for most projects
  • Understanding auth fundamentals makes you a better developer

How our auth works:

Login Request → Validate credentials → Create JWT → Set HTTP-only cookie
                                                           │
Subsequent requests → Middleware reads cookie → Verifies JWT → Allows/denies
                                                                    │
API routes → getCurrentUser() reads cookie → Returns user or null

Key security features:

  • HTTP-only cookies — JavaScript can't access the token (XSS protection)
  • Secure flag — Cookie only sent over HTTPS in production
  • SameSite=Lax — CSRF protection without breaking OAuth redirects
  • Short expiry — Tokens expire after 7 days (configurable)
  • Password hashing — Uses Web Crypto API's PBKDF2 (no bcrypt dependency)

4. Prisma ORM for Database Access

We use Prisma because:

  • Type safety — Prisma generates TypeScript types from your schema. prisma.user.findUnique() returns a fully typed User object.
  • Migrationsprisma migrate dev handles schema changes with version-controlled SQL migrations.
  • Multi-database — Switch between PostgreSQL, MySQL, SQLite, and MongoDB by changing the provider in schema.prisma.
  • Query logging — Enable query logging in development to see the actual SQL.

The singleton pattern (src/lib/prisma.ts):

typescript
// Without singleton: Hot Module Replacement creates a new PrismaClient
// on every file save, eventually exhausting database connections.
//
// With singleton: One PrismaClient instance is stored on `globalThis`
// and reused across hot reloads.

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  globalForPrisma.prisma = prisma;
}

5. Edge Middleware for Route Protection

The middleware.ts file runs on Vercel's Edge Runtime (or Node.js locally). It intercepts every request before it reaches your pages.

Why middleware instead of per-page auth checks?

  • Centralized — One file protects all routes, no chance of forgetting a page
  • Fast — Edge middleware runs in <1ms, before the page starts rendering
  • Redirects — Unauthenticated users are redirected to login, not shown a 403

Route matching strategy:

typescript
// Protected routes — require authentication
const PROTECTED_PREFIXES = ["/dashboard", "/settings", "/api/users"];

// Auth routes — redirect to dashboard if already logged in
const AUTH_ROUTES = ["/login", "/signup"];

The middleware checks the JWT cookie, verifies it, and either allows the request or redirects. It does NOT query the database (that would be slow on every request). The JWT contains the user's ID and role, which is enough for authorization decisions.

6. ISR (Incremental Static Regeneration) for Blog

The blog pages use ISR — a hybrid between static and dynamic rendering:

typescript
// app/blog/page.tsx
export const revalidate = 60; // Regenerate at most every 60 seconds

How ISR works:

1. First visitor → Next.js renders the page server-side and caches it

2. Next 60 seconds → All visitors get the cached HTML (instant)

3. After 60 seconds → Next visitor triggers a background regeneration

4. Background regeneration completes → New HTML replaces the cache

5. All subsequent visitors get the fresh page

This gives you the performance of a static site with the freshness of server rendering. Blog posts update within 60 seconds of a database change, without a full rebuild.

generateStaticParams for dynamic routes:

typescript
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await prisma.post.findMany({
    where: { published: true },
    select: { slug: true },
  });
  return posts.map((post) => ({ slug: post.slug }));
}

This pre-generates pages for all existing posts at build time. New posts are rendered on-demand and then cached.

Data Flow Patterns

Pattern 1: Server Component Data Fetching

User visits /blog → Server Component renders
                          │
                    Prisma query (server-side, no API call)
                          │
                    Returns HTML with post data
                          │
                    Browser displays page (zero JS for data fetching)

Pattern 2: Client-Side Mutations via API Routes

User clicks "Create Post" → Client Component form
                                    │
                              fetch("/api/posts", { method: "POST", body: ... })
                                    │
                              API route validates → Prisma insert → Returns JSON
                                    │
                              Client updates UI (optimistic or after response)

Pattern 3: Authenticated API Calls

Client sends request → API route handler
                              │
                        getCurrentUser(request) reads cookie
                              │
                        JWT valid? → Query database with user context
                        JWT invalid? → Return 401

Scaling the Architecture

Adding a New Protected Page

1. Create app/your-page/page.tsx

2. Add the route prefix to PROTECTED_PREFIXES in middleware.ts

3. Use getCurrentUser() in the page for user-specific data

Adding a New API Endpoint

1. Create app/api/your-resource/route.ts

2. Export GET, POST, PUT, or DELETE functions

3. Use getCurrentUser(request) for authenticated endpoints

4. Return NextResponse.json() with appropriate status codes

Adding a New Database Model

1. Add the model to prisma/schema.prisma

2. Run npx prisma migrate dev --name add_your_model

3. Add the TypeScript type to src/types/index.ts

4. Create an API route at app/api/your-model/route.ts

Adding Real-Time Features

For real-time updates (chat, notifications, live dashboards), you have several options:

1. Server-Sent Events (SSE) — Simple, one-way server-to-client. Create a streaming API route.

2. WebSockets — Two-way communication. Requires a separate WebSocket server (not supported on Vercel Serverless).

3. Polling — Simple but less efficient. Use setInterval with fetch() in a client component.

4. Third-party services — Pusher, Ably, or Supabase Realtime for managed WebSockets.

Adding Team/Organization Support

The current schema has individual users. To add teams:

1. Add Team and TeamMember models to Prisma

2. Add teamId to the JWT payload

3. Update middleware to check team membership

4. Add team-scoped queries (where: { teamId })

Performance Considerations

OptimizationImplementationImpact
Server ComponentsDefault — no "use client" unless needed50-80% less client JS
ISR for blogrevalidate = 60 on listing pagesStatic-speed for reads
Image optimizationUse next/image with width/heightAutomatic WebP, lazy loading
Font optimizationUse next/font/google in root layoutNo layout shift, preloaded
Route prefetchingAutomatic for in viewportInstant navigation
Edge middlewareAuth checks at the edge, not in pages<1ms auth overhead
Prisma singletonSingle connection pool across hot reloadsNo connection exhaustion

Security Checklist

  • [x] Passwords hashed with PBKDF2 (100,000 iterations)
  • [x] JWT stored in HTTP-only, Secure, SameSite=Lax cookie
  • [x] CSRF protection via SameSite cookie attribute
  • [x] Route protection in edge middleware (not just client-side)
  • [x] Input validation in API routes before database queries
  • [x] Error responses don't leak internal details
  • [x] Environment variables for all secrets (never hardcoded)
  • [ ] Rate limiting on auth endpoints (add with Upstash or similar)
  • [ ] Content Security Policy headers (add in next.config.ts)
  • [ ] CORS configuration for API routes (add if serving external clients)

Part of Frontend Developer Pro

Chapter 2

Deployment Guide

This guide covers deploying your Next.js starter kit to production. We cover Vercel (recommended), Docker, and traditional Node.js hosting.

Prerequisites

Before deploying, ensure you have:

1. A PostgreSQL database — Vercel Postgres, Supabase, Neon, PlanetScale, or any PostgreSQL provider

2. Environment variables — Copy .env.example to .env and fill in all values

3. Prisma migrations applied — Run npx prisma migrate deploy against your production database

Vercel is the company behind Next.js. Their platform is optimized for Next.js applications with zero-config deployments.

Step-by-Step

bash
# 1. Install the Vercel CLI
npm i -g vercel

# 2. Link your project (first time only)
vercel link

# 3. Set environment variables
vercel env add DATABASE_URL        # Your PostgreSQL connection string
vercel env add JWT_SECRET          # Random 64-character string
vercel env add NEXT_PUBLIC_APP_URL # https://your-domain.com

# 4. Deploy to preview
vercel

# 5. Deploy to production
vercel --prod

Vercel Configuration

The included vercel.json handles:

jsonc
{
  // Redirect www to non-www (or vice versa)
  // Security headers on all routes
  // Cache control for static assets
  // Function timeout and memory limits
}

Environment Variables on Vercel

Set these in the Vercel dashboard (Settings → Environment Variables) or via CLI:

VariableDescriptionExample
DATABASE_URLPostgreSQL connection stringpostgresql://user:pass@host:5432/db?sslmode=require
JWT_SECRETSecret for signing JWTs (min 32 chars)Generate with openssl rand -hex 32
NEXT_PUBLIC_APP_URLYour production URLhttps://your-app.vercel.app
NODE_ENVEnvironment flagproduction (Vercel sets this automatically)

Database Setup with Vercel Postgres

If you're using Vercel Postgres:

bash
# 1. Create the database in the Vercel dashboard
# Storage → Create → Postgres

# 2. Vercel automatically adds DATABASE_URL to your environment

# 3. Run migrations
npx prisma migrate deploy

# 4. (Optional) Seed initial data
npx prisma db seed

Custom Domain

bash
# Add your domain
vercel domains add your-domain.com

# Vercel will provide DNS records to configure:
# - A record: 76.76.21.21
# - CNAME: cname.vercel-dns.com (for www)

Preview Deployments

Every push to a non-production branch creates a preview deployment with:

  • A unique URL (e.g., your-app-git-feature-branch.vercel.app)
  • The same environment variables as production (or separate preview variables)
  • Automatic Lighthouse score comments on PRs (if GitHub integration is enabled)

Option 2: Docker

For self-hosting or cloud providers that support containers (AWS ECS, Google Cloud Run, DigitalOcean App Platform, Railway).

Dockerfile

Create a Dockerfile in your project root:

dockerfile
# =============================================================================
# Multi-stage Docker build for Next.js
# =============================================================================
# Stage 1: Install dependencies
# Stage 2: Build the application
# Stage 3: Production runtime (minimal image)
# =============================================================================

# --- Stage 1: Dependencies ---
FROM node:20-alpine AS deps
WORKDIR /app

# Copy package files first for better layer caching
# If package.json hasn't changed, Docker reuses this layer
COPY package.json package-lock.json ./
COPY prisma ./prisma/

RUN npm ci --omit=dev
RUN npx prisma generate

# --- Stage 2: Build ---
FROM node:20-alpine AS builder
WORKDIR /app

COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects anonymous telemetry data about usage.
# Disable it in Docker builds to avoid network calls during build.
ENV NEXT_TELEMETRY_DISABLED=1

RUN npm run build

# --- Stage 3: Production Runtime ---
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1

# Create a non-root user for security
# Running as root in a container is a security risk — if the app is
# compromised, the attacker has root access to the container.
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# Copy only the files needed for production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/prisma ./prisma
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma

USER nextjs

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

# Health check — Docker and orchestrators use this to know if the
# container is healthy. If the health check fails, the container is
# restarted automatically.
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1

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

Enable Standalone Output

Add this to your next.config.ts:

typescript
const nextConfig = {
  output: "standalone",
  // ... rest of your config
};

The standalone output creates a self-contained server.js that doesn't need node_modules. This reduces the Docker image size from ~500MB to ~100MB.

Build and Run

bash
# Build the image
docker build -t nextjs-starter .

# Run locally
docker run -p 3000:3000 \
  -e DATABASE_URL="postgresql://user:pass@host:5432/db" \
  -e JWT_SECRET="your-secret-here" \
  -e NEXT_PUBLIC_APP_URL="http://localhost:3000" \
  nextjs-starter

# Run database migrations (one-time)
docker run --rm \
  -e DATABASE_URL="postgresql://user:pass@host:5432/db" \
  nextjs-starter \
  npx prisma migrate deploy

Docker Compose (for local development with database)

Create a docker-compose.yml:

yaml
version: "3.9"

services:
  # PostgreSQL database
  db:
    image: postgres:16-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: nextjs_starter
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U devuser -d nextjs_starter"]
      interval: 5s
      timeout: 5s
      retries: 5

  # Next.js application
  app:
    build: .
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: "postgresql://devuser:devpassword@db:5432/nextjs_starter"
      JWT_SECRET: "local-development-secret-change-in-production"
      NEXT_PUBLIC_APP_URL: "http://localhost:3000"
    depends_on:
      db:
        condition: service_healthy

volumes:
  postgres_data:
bash
# Start everything
docker compose up -d

# Run migrations
docker compose exec app npx prisma migrate deploy

# View logs
docker compose logs -f app

# Stop everything
docker compose down

Option 3: Traditional Node.js Hosting

For VPS providers (DigitalOcean Droplets, Hetzner, Linode) or any server with Node.js.

Build and Start

bash
# Install dependencies
npm ci

# Generate Prisma client
npx prisma generate

# Run database migrations
npx prisma migrate deploy

# Build the application
npm run build

# Start the production server
npm start
# → Listening on http://localhost:3000

Process Manager (PM2)

Use PM2 to keep your app running and restart on crashes:

bash
# Install PM2 globally
npm i -g pm2

# Start the app with PM2
pm2 start npm --name "nextjs-starter" -- start

# Configure auto-restart on server reboot
pm2 startup
pm2 save

# Monitor
pm2 monit

# View logs
pm2 logs nextjs-starter

PM2 Ecosystem File

Create ecosystem.config.js for more control:

javascript
module.exports = {
  apps: [
    {
      name: "nextjs-starter",
      script: "npm",
      args: "start",
      instances: "max",        // Use all CPU cores
      exec_mode: "cluster",    // Cluster mode for load balancing
      env_production: {
        NODE_ENV: "production",
        PORT: 3000,
      },
      // Restart if memory exceeds 512MB
      max_memory_restart: "512M",
      // Graceful shutdown
      kill_timeout: 5000,
      // Log configuration
      log_date_format: "YYYY-MM-DD HH:mm:ss Z",
      error_file: "./logs/error.log",
      out_file: "./logs/output.log",
      merge_logs: true,
    },
  ],
};

Nginx Reverse Proxy

Nginx sits in front of your Node.js app, handling SSL, static files, and load balancing:

nginx
# /etc/nginx/sites-available/nextjs-starter

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name your-domain.com www.your-domain.com;
    return 301 https://$server_name$request_uri;
}

# Main HTTPS server
server {
    listen 443 ssl http2;
    server_name your-domain.com;

    # SSL certificates (use Let's Encrypt / certbot)
    ssl_certificate /etc/letsencrypt/live/your-domain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/your-domain.com/privkey.pem;

    # SSL configuration (Mozilla Modern profile)
    ssl_protocols TLSv1.3;
    ssl_prefer_server_ciphers off;

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css application/json application/javascript
               text/xml application/xml application/xml+rss text/javascript
               image/svg+xml;

    # Next.js static files — served directly by Nginx (faster than Node.js)
    location /_next/static/ {
        alias /var/www/nextjs-starter/.next/static/;
        expires 365d;
        access_log off;
        add_header Cache-Control "public, immutable";
    }

    # Public files
    location /public/ {
        alias /var/www/nextjs-starter/public/;
        expires 30d;
        access_log off;
    }

    # Proxy everything else to Next.js
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

SSL with Let's Encrypt

bash
# Install certbot
sudo apt install certbot python3-certbot-nginx

# Get SSL certificate (auto-configures Nginx)
sudo certbot --nginx -d your-domain.com -d www.your-domain.com

# Auto-renewal is configured automatically via systemd timer
# Verify with:
sudo certbot renew --dry-run

Database Migration Strategy

Development

bash
# Create a migration after schema changes
npx prisma migrate dev --name describe_your_change

# This does three things:
# 1. Generates a SQL migration file in prisma/migrations/
# 2. Applies the migration to your dev database
# 3. Regenerates the Prisma client

Production

bash
# Apply pending migrations (non-interactive, safe for CI/CD)
npx prisma migrate deploy

# This ONLY applies existing migrations — it never creates new ones.
# If a migration fails, it stops and reports the error.

Migration in CI/CD Pipeline

Add to your deployment script (GitHub Actions example):

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci

      # Run migrations before deployment
      # If this fails, the deployment is aborted
      - name: Run database migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

      - name: Build
        run: npm run build

      # Deploy to your platform of choice
      - name: Deploy to Vercel
        run: vercel --prod --token ${{ secrets.VERCEL_TOKEN }}

Environment-Specific Configuration

Development

env
# .env.local (never committed)
DATABASE_URL="postgresql://devuser:devpassword@localhost:5432/nextjs_dev"
JWT_SECRET="development-only-secret-not-for-production"
NEXT_PUBLIC_APP_URL="http://localhost:3000"

Staging

env
DATABASE_URL="postgresql://staging_user:xxx@staging-db.example.com:5432/nextjs_staging?sslmode=require"
JWT_SECRET="staging-secret-different-from-production"
NEXT_PUBLIC_APP_URL="https://staging.your-domain.com"

Production

env
DATABASE_URL="postgresql://prod_user:xxx@prod-db.example.com:5432/nextjs_prod?sslmode=require&connection_limit=20"
JWT_SECRET="cryptographically-random-64-char-string"
NEXT_PUBLIC_APP_URL="https://your-domain.com"

Monitoring and Observability

Vercel Analytics

bash
# Install Vercel analytics
npm i @vercel/analytics @vercel/speed-insights

Add to your root layout:

typescript
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

Health Check Endpoint

Add a health check for load balancers and monitoring:

typescript
// app/api/health/route.ts
import { prisma } from "@/lib/prisma";
import { NextResponse } from "next/server";

export async function GET() {
  try {
    // Check database connectivity
    await prisma.$queryRaw`SELECT 1`;

    return NextResponse.json({
      status: "healthy",
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
    });
  } catch (error) {
    return NextResponse.json(
      {
        status: "unhealthy",
        timestamp: new Date().toISOString(),
        error: "Database connection failed",
      },
      { status: 503 }
    );
  }
}

Error Tracking

For production error tracking, consider:

1. Sentrynpm i @sentry/nextjs with automatic source maps

2. LogRocket — Session replay + error tracking

3. Axiom — Log aggregation with Vercel integration

Performance Optimization Checklist

Before going to production:

  • [ ] Enable output: "standalone" in next.config.ts (for Docker)
  • [ ] Set images.domains for allowed external image sources
  • [ ] Add Cache-Control headers for API routes that can be cached
  • [ ] Enable ISR on listing pages with appropriate revalidate values
  • [ ] Use next/font for Google Fonts (eliminates layout shift)
  • [ ] Use next/image for all images (automatic optimization)
  • [ ] Lazy-load below-the-fold components with dynamic()
  • [ ] Run npx next build locally and check the output for large pages
  • [ ] Set up Vercel Analytics or Web Vitals reporting
  • [ ] Test with Lighthouse (aim for 90+ on all categories)
  • [ ] Enable gzip/brotli compression (Vercel does this automatically)
  • [ ] Use connection pooling for the database (PgBouncer or Prisma Accelerate)

Troubleshooting

"PrismaClientInitializationError: Can't reach database server"

1. Check that DATABASE_URL is set correctly in your environment

2. Verify the database server is running and accepting connections

3. Check firewall rules — your deployment server must be able to reach the database

4. If using Vercel, ensure the database allows connections from Vercel's IP ranges

"Error: JWT_SECRET is not defined"

The app requires a JWT_SECRET environment variable. Generate one:

bash
openssl rand -hex 32

Set it in your deployment environment (Vercel dashboard, Docker env, or .env file).

"Build fails with TypeScript errors"

Run the build locally first to reproduce:

bash
npm run build

Fix all TypeScript errors before deploying. The strict: true setting in tsconfig.json catches many issues at build time.

"CORS errors when calling API routes"

By default, Next.js API routes only accept same-origin requests. If you're calling from a different domain:

typescript
// Add to your API route handler
export async function GET(request: Request) {
  const response = NextResponse.json(data);
  response.headers.set("Access-Control-Allow-Origin", "https://your-frontend.com");
  response.headers.set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
  return response;
}

Part of Frontend Developer Pro

Next.js Starter Kit v1.0.0 — Free Preview