Click any item to expand the explanation and examples.
📁 File Conventions (App Router)
page.tsx — route page files
page.tsx. The file path = the URL.
app/page.tsx → / app/about/page.tsx → /about app/blog/page.tsx → /blog app/blog/[slug]/page.tsx → /blog/my-post
// app/page.tsx
export default function Home() {
return <h1>Home</h1>;
}
layout.tsx — shared layout files
// app/layout.tsx (root layout — required)
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
// app/dashboard/layout.tsx (nested layout)
export default function DashboardLayout({ children }) {
return (
<div>
<nav>Dashboard Nav</nav>
{children}
</div>
);
}
loading.tsx, error.tsx, not-found.tsx files
// app/dashboard/loading.tsx — shows while page loads
export default function Loading() {
return <div>Loading...</div>;
}
// app/dashboard/error.tsx — catches errors
‘use client’;
export default function Error({ error, reset }) {
return (
<div>
<p>Something went wrong: {error.message}</p>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/not-found.tsx — custom 404
export default function NotFound() {
return <h1>Page not found</h1>;
}
route.ts — API routes files
// app/api/users/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
const users = await db.user.findMany();
return NextResponse.json(users);
}
export async function POST(request: Request) {
const body = await request.json();
const user = await db.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
}
Supported methods: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS.
🔀 Routing
Dynamic routes — [slug] routing
// app/blog/[slug]/page.tsx
export default async function Post({ params }) {
const { slug } = await params;
return <h1>Post: {slug}</h1>;
}
// Catch-all: app/docs/[…slug]/page.tsx
// Matches /docs/a, /docs/a/b, /docs/a/b/c
export default async function Docs({ params }) {
const { slug } = await params; // [‘a’, ‘b’, ‘c’]
}
// Optional catch-all: app/docs/[[…slug]]/page.tsx
// Also matches /docs (without any slug)
Route groups — (folder) routing
app/(marketing)/about/page.tsx → /about app/(marketing)/pricing/page.tsx → /pricing app/(app)/dashboard/page.tsx → /dashboardEach group can have its own layout
app/(marketing)/layout.tsx app/(app)/layout.tsx
Parallel & intercepting routes routing
# Parallel routes — render multiple pages in same layout
app/@modal/login/page.tsx
app/layout.tsx → receives { children, modal } props
Intercepting routes — show route in modal
app/feed/@modal/(.)photo/[id]/page.tsx
(.) = same level, (..) = one level up, (…) = root
📡 Data Fetching
Server Components — fetch in components data
async/await.
// app/users/page.tsx (Server Component)
export default async function UsersPage() {
const res = await fetch('https://api.example.com/users');
const users = await res.json();
return (
<ul>
{users.map(u => <li key={u.id}>{u.name}</li>)}
</ul>
);
}
Caching & revalidation data
// Cache forever (default)
fetch('https://api.example.com/data');
// Revalidate every 60 seconds
fetch(‘https://api.example.com/data’, {
next: { revalidate: 60 }
});
// No cache (always fresh)
fetch(‘https://api.example.com/data’, {
cache: ‘no-store’
});
// Page-level revalidation
export const revalidate = 60;
// On-demand revalidation (in a Server Action or Route Handler)
import { revalidatePath, revalidateTag } from ‘next/cache’;
revalidatePath(‘/blog’);
revalidateTag(‘posts’);
Server Actions data
// app/actions.ts 'use server';export async function createPost(formData: FormData) { const title = formData.get(‘title’); await db.post.create({ data: { title } }); revalidatePath(‘/blog’); }
// In a component import { createPost } from ’./actions’;
export default function NewPost() { return ( <form action={createPost}> <input name=“title” /> <button type=“submit”>Create</button> </form> ); }
🧩 Common Patterns
'use client' — Client Components pattern
'use client' at the top when you need interactivity.
'use client';You needimport { useState } from ‘react’;
export default function Counter() { const [count, setCount] = useState(0); return <button onClick={() => setCount(c => c + 1)}>{count}</button>; }
‘use client’ for: useState, useEffect, event handlers, browser APIs.
Metadata & SEO pattern
// Static metadata
export const metadata = {
title: 'My Page',
description: 'Page description',
openGraph: { title: 'My Page', images: ['/og.png'] },
};
// Dynamic metadata
export async function generateMetadata({ params }) {
const post = await getPost(params.slug);
return { title: post.title, description: post.excerpt };
}
Middleware pattern
// middleware.ts (in project root)
import { NextResponse } from 'next/server';
export function middleware(request) {
// Redirect
if (request.nextUrl.pathname === ‘/old’) {
return NextResponse.redirect(new URL(‘/new’, request.url));
}
// Rewrite
if (request.nextUrl.pathname.startsWith(‘/api’)) {
return NextResponse.rewrite(new URL(‘/api/v2’ + request.nextUrl.pathname, request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [‘/old’, ‘/api/:path*’],
};
Environment variables pattern
# .env.local DATABASE_URL=postgresql://... NEXT_PUBLIC_API_URL=https://api.example.comOnly variables prefixed withServer-only (no prefix)
process.env.DATABASE_URL
Client-accessible (NEXT_PUBLIC_ prefix)
process.env.NEXT_PUBLIC_API_URL
NEXT_PUBLIC_ are available in the browser. Everything else is server-only.