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.
├── 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
We use the App Router (introduced in Next.js 13, stable in 14) instead of the legacy Pages Router. Here's why:
| Feature | Pages Router | App Router (this starter) |
|---|---|---|
| Rendering model | Pages default to client-side | Components default to server-side |
| Data fetching | getServerSideProps, getStaticProps | async Server Components, fetch() with caching |
| Layouts | Custom _app.tsx wrapper | Nested layout.tsx files (preserved across navigation) |
| Loading states | Manual implementation | Built-in loading.tsx per route segment |
| Error handling | Custom _error.tsx | Built-in error.tsx per route segment |
| Metadata | Manual management | metadata export or generateMetadata() |
| Streaming | Not supported | Built-in with |
The App Router is the future of Next.js. All new features ship for App Router first.
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":
onClick, onChange, onSubmit)useState, useEffect, useRef, usePathname)In this starter, only Header.tsx needs "use client" (for the mobile menu toggle and active link detection).
We implement authentication from scratch using JWTs instead of using NextAuth.js. This is deliberate:
Why not NextAuth?
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:
We use Prisma because:
prisma.user.findUnique() returns a fully typed User object.prisma migrate dev handles schema changes with version-controlled SQL migrations.provider in schema.prisma.The singleton pattern (src/lib/prisma.ts):
// 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;
}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?
Route matching strategy:
// 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.
The blog pages use ISR — a hybrid between static and dynamic rendering:
// app/blog/page.tsx
export const revalidate = 60; // Regenerate at most every 60 secondsHow 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:
// 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.
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)
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)
Client sends request → API route handler
│
getCurrentUser(request) reads cookie
│
JWT valid? → Query database with user context
JWT invalid? → Return 401
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
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
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
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.
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 })
| Optimization | Implementation | Impact |
|---|---|---|
| Server Components | Default — no "use client" unless needed | 50-80% less client JS |
| ISR for blog | revalidate = 60 on listing pages | Static-speed for reads |
| Image optimization | Use next/image with width/height | Automatic WebP, lazy loading |
| Font optimization | Use next/font/google in root layout | No layout shift, preloaded |
| Route prefetching | Automatic for in viewport | Instant navigation |
| Edge middleware | Auth checks at the edge, not in pages | <1ms auth overhead |
| Prisma singleton | Single connection pool across hot reloads | No connection exhaustion |
next.config.ts)Part of Frontend Developer Pro
Get the full Next.js Starter Kit and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.