Next.js Framework Guide
Framework: Next.js 14+ (App Router) Language: TypeScript/JavaScript Use Cases: Full-Stack Web Apps, SSR/SSG, E-commerce, Blogs, Dashboards
Overview
Next.js is a React framework providing server-side rendering, static site generation, API routes, and full-stack development in a single codebase. Version 14+ uses the App Router as the default, built on React Server Components.
Project Setup
# Create new Next.js app npx create-next-app@latest my-app --typescript --tailwind --eslint --app cd my-app npm run dev
Recommended Project Structure
my-app/ ├── app/ │ ├── (auth)/ # Route group (no URL segment) │ │ ├── login/page.tsx │ │ └── register/page.tsx │ ├── dashboard/ │ │ ├── page.tsx # /dashboard │ │ ├── loading.tsx # Loading UI │ │ ├── error.tsx # Error boundary │ │ └── layout.tsx # Dashboard layout │ ├── api/ │ │ └── users/route.ts # API route handler │ ├── globals.css │ ├── layout.tsx # Root layout (required) │ └── page.tsx # Home page (/) ├── components/ │ ├── ui/ # Reusable UI components │ └── features/ # Feature-specific components ├── lib/ │ ├── db.ts # Database client │ └── utils.ts # Utility functions ├── hooks/ # Custom React hooks ├── types/ # TypeScript type definitions ├── public/ # Static assets ├── middleware.ts # Edge middleware ├── next.config.js ├── tailwind.config.ts └── package.json
Routing (App Router)
File-Based Routing Conventions
| File | Purpose |
|---|---|
page.tsx | Route UI (makes segment publicly accessible) |
layout.tsx | Shared layout (wraps children, persists) |
loading.tsx | Loading UI (Suspense boundary) |
error.tsx | Error boundary (must be 'use client') |
not-found.tsx | 404 UI for this segment |
route.ts | API route handler (GET, POST, etc.) |
template.tsx | Like layout but re-mounts on navigation |
default.tsx | Fallback for parallel routes |
Route Patterns
app/
├── page.tsx # /
├── about/page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/page.tsx # /blog/:slug (dynamic)
├── shop/
│ └── [...categories]/page.tsx # /shop/a/b/c (catch-all)
├── (marketing)/ # Route group (no URL impact)
│ ├── pricing/page.tsx # /pricing
│ └── features/page.tsx # /features
└── @modal/ # Parallel route (named slot)
└── login/page.tsx
Page Component with Params
// app/blog/[slug]/page.tsx
interface PageProps {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}
export default function BlogPost({ params, searchParams }: PageProps) {
return <article><h1>Post: {params.slug}</h1></article>;
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await getPost(params.slug);
return { title: post.title, description: post.excerpt };
}
Layouts
// app/layout.tsx -- Root Layout (required, wraps entire app)
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'My application',
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
Nested layouts compose automatically. Dashboard layout wraps all /dashboard/* routes:
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar />
<div className="flex-1">{children}</div>
</div>
);
}
Server Components vs Client Components
Decision Rule
| Need | Component Type |
|---|---|
| Fetch data, access backend resources | Server (default) |
| Static rendering, SEO content | Server |
| Use hooks (useState, useEffect, etc.) | Client |
| Browser APIs (window, localStorage) | Client |
| Event handlers (onClick, onChange) | Client |
| Third-party client-only libraries | Client |
Server Component (Default)
All components in the app/ directory are Server Components by default. They run on the server only and can directly access databases, file systems, and secrets.
// app/users/page.tsx -- Server Component (no directive needed)
import { db } from '@/lib/db';
export default async function UsersPage() {
const users = await db.user.findMany();
return (
<ul>
{users.map((user) => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Client Component
Add 'use client' at the top of the file. Push this directive as low in the tree as possible.
// components/Counter.tsx
'use client';
import { useState } from 'react';
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
Composition Pattern
Fetch data in Server Components, pass to Client Components as props:
// app/dashboard/page.tsx (Server Component)
import { ClientSidebar } from '@/components/ClientSidebar';
import { db } from '@/lib/db';
export default async function Dashboard() {
const stats = await db.stats.get();
return (
<div>
<ClientSidebar initialStats={stats} />
<DashboardContent stats={stats} />
</div>
);
}
Data Fetching
Server Component Fetch with Caching
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // ISR: revalidate every hour
});
if (!res.ok) throw new Error('Failed to fetch products');
return res.json();
}
Fetch Caching Options
| Option | Behavior |
|---|---|
{ cache: 'force-cache' } | Static (default for GET) |
{ cache: 'no-store' } | Dynamic (no caching) |
{ next: { revalidate: N } } | ISR (revalidate every N seconds) |
{ next: { tags: ['posts'] } } | Tag-based revalidation |
Parallel Fetching
Always fetch independent data in parallel with Promise.all:
export default async function Dashboard({ params }: { params: { id: string } }) {
const [user, orders] = await Promise.all([
getUser(params.id),
getOrders(params.id),
]);
return <div><UserProfile user={user} /><OrderList orders={orders} /></div>;
}
Streaming with Suspense
import { Suspense } from 'react';
export default function Dashboard() {
return (
<div>
<WelcomeMessage />
<Suspense fallback={<StatsSkeleton />}>
<Stats />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
Server Actions
Define mutations with 'use server'. They run on the server and can be called from forms or client code.
// app/actions.ts
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { z } from 'zod';
const createPostSchema = z.object({
title: z.string().min(1),
content: z.string().min(10),
});
export async function createPost(formData: FormData) {
const validated = createPostSchema.parse({
title: formData.get('title'),
content: formData.get('content'),
});
await db.post.create({ data: validated });
revalidatePath('/posts');
redirect(`/posts`);
}
Use in a form (no client JavaScript required for basic submissions):
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<textarea name="content" required />
<button type="submit">Create</button>
</form>
);
}
API Route Handlers
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
export async function GET(request: NextRequest) {
const page = parseInt(request.nextUrl.searchParams.get('page') || '1');
const users = await db.user.findMany({ skip: (page - 1) * 10, take: 10 });
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validated = userSchema.parse(body);
const user = await db.user.create({ data: validated });
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ errors: error.errors }, { status: 400 });
}
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
Middleware
Runs at the edge before every matched request. Use for auth checks, redirects, headers.
// middleware.ts (project root)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
const isProtected = request.nextUrl.pathname.startsWith('/dashboard');
if (isProtected && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};
Error Handling
Error Boundary (error.tsx)
// app/dashboard/error.tsx
'use client';
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
return (
<div>
<h2>Something went wrong</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Not Found
// app/not-found.tsx
import Link from 'next/link';
export default function NotFound() {
return (
<div>
<h2>Not Found</h2>
<Link href="/">Return Home</Link>
</div>
);
}
Trigger programmatically: import { notFound } from 'next/navigation'; notFound();
Configuration
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [{ protocol: 'https', hostname: '**.example.com' }],
},
async redirects() {
return [{ source: '/old', destination: '/new', permanent: true }];
},
async headers() {
return [{
source: '/api/:path*',
headers: [{ key: 'Access-Control-Allow-Origin', value: '*' }],
}];
},
};
module.exports = nextConfig;
Guardrails
- •Use Server Components by default; add
'use client'only when needed - •Push
'use client'as low in the component tree as possible - •Colocate data fetching with the component that needs it
- •Use
Promise.allfor independent parallel fetches - •Implement
loading.tsxanderror.tsxfor every route segment - •Use Server Actions for mutations (not API routes for form submissions)
- •Validate all inputs with schema validators (Zod) in Server Actions and API routes
- •Use
next/imagefor images andnext/fontfor fonts (performance) - •Set proper metadata on every page for SEO
- •Use Suspense boundaries to stream slow data
- •Never import server-only modules in Client Components
- •Never expose secrets or database access in Client Components
Commands Reference
npm run dev # Development server (http://localhost:3000) npm run build # Production build npm run start # Start production server npm run lint # ESLint check npx tsc --noEmit # TypeScript validation npm test # Run tests (Vitest/Jest)
Advanced Topics
For detailed code examples, advanced patterns, testing strategies, performance optimization, caching strategies, and ISR/SSG/SSR details, see:
- •references/patterns.md -- Authentication, advanced Server Actions, testing, performance, caching, rendering strategies, deployment