File Conventions
| File | Purpose |
|---|
page.tsx | Route UI (required for route) |
layout.tsx | Shared layout, wraps children |
loading.tsx | Loading state (Suspense boundary) |
error.tsx | Error boundary (must be client) |
not-found.tsx | 404 page |
route.ts | API route handler |
// error.tsx - must be client component
'use client';
export default function Error({ error, reset }) {
return <button onClick={reset}>Try again</button>;
}
Routing
| Pattern | Syntax | Example |
|---|
| Dynamic | [id] | /blog/[id] → /blog/123 |
| Catch-all | [...slug] | /docs/[...slug] → /docs/a/b/c |
| Optional catch-all | [[...slug]] | Matches /docs and /docs/a |
| Route groups | (name) | (auth)/login → /login (no URL segment) |
| Parallel routes | @slot | @modal + @sidebar in layout |
| Intercepting | (.), (..) | (.)photo/[id] intercepts /photo/1 |
// app/blog/[id]/page.tsx
export default function Page({ params }: { params: { id: string } }) {
return <h1>Post {params.id}</h1>;
}
// route groups: app/(marketing)/about/page.tsx → /about
// intercepting: app/photo/(.)photo/[id]/page.tsx → modal overlay
Server vs Client Components
| Type | Default | Use |
|---|
| Server | Yes | Data fetch, layout, static content |
| Client | 'use client' | useState, useEffect, event handlers |
// Server (default) - can async, fetch directly
async function ServerPage() {
const data = await fetch('...');
return <div>{data}</div>;
}
// Client - add at top of file
'use client';
function ClientButton() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>{n}</button>;
}
Data Fetching
| Option | Behavior |
|---|
| Default | Cached (static at build) |
revalidate: 60 | ISR, revalidate every 60s |
cache: 'no-store' | Dynamic, no cache |
next: { tags: ['x'] } | Tag for on-demand revalidation |
// Static (cached)
const res = await fetch('https://api.example.com/data');
// ISR - revalidate every 60 seconds
const res = await fetch('https://...', { next: { revalidate: 60 } });
// Dynamic - always fresh
const res = await fetch('https://...', { cache: 'no-store' });
// On-demand revalidation
import { revalidateTag, revalidatePath } from 'next/cache';
revalidateTag('posts');
revalidatePath('/blog');
Server Actions
// app/actions.ts
'use server';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
// mutate DB, revalidate
revalidatePath('/blog');
return { success: true };
}
// In component
import { createPost } from './actions';
<form action={createPost}>
<input name="title" />
<button type="submit">Submit</button>
</form>
Middleware
// middleware.ts at project root
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
if (request.nextUrl.pathname.startsWith('/admin')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*'],
};
Metadata API
// Static - layout.tsx or page.tsx
export const metadata = {
title: 'My App',
description: 'App description',
openGraph: { title: '...', images: ['/og.png'] },
};
// Dynamic - page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.id);
return { title: post.title, description: post.excerpt };
}
Image Optimization
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero"
width={800}
height={600}
priority
placeholder="blur"
blurDataURL="data:image/..."
/>
// Remote - add domain to next.config.js
<Image src="https://example.com/img.png" width={500} height={300} />
Link Component
import Link from 'next/link';
<Link href="/blog">Blog</Link>
<Link href={`/blog/${id}`}>Post</Link>
<Link href="/dashboard" prefetch={false}>Dashboard</Link>
Environment Variables
| Prefix | Exposed to |
|---|
| (none) | Server only |
NEXT_PUBLIC_ | Server + Client |
# .env.local
DATABASE_URL=postgres://...
NEXT_PUBLIC_API_URL=https://api.example.com
// Server
const db = process.env.DATABASE_URL;
// Client (must have NEXT_PUBLIC_)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;