- File conventions:
page.tsx,layout.tsx,loading.tsx,error.tsx,not-found.tsx,route.ts - Server Components are default — add
"use client"only when you need browser APIs or interactivity - Data fetching:
fetch()in Server Components (nouseEffect),use()hook in Client Components - Server Actions:
"use server"functions called directly from forms and client code - Next.js 15 breaking change:
paramsandsearchParamsare now Promises — must beawait-ed
App directory file conventions
app/
├── layout.tsx # Root layout (wraps everything, required)
├── page.tsx # Route: /
├── loading.tsx # Shown while page.tsx suspends
├── error.tsx # Error boundary for this segment
├── not-found.tsx # Shown when notFound() is called
├── template.tsx # Like layout but re-mounts on navigation
├── global-error.tsx # Catches errors in root layout
│
├── about/
│ └── page.tsx # Route: /about
│
├── blog/
│ ├── layout.tsx # Layout for /blog and all children
│ ├── page.tsx # Route: /blog
│ └── [slug]/
│ └── page.tsx # Route: /blog/:slug (dynamic)
│
├── api/
│ └── users/
│ └── route.ts # Route handler: GET/POST /api/users
│
└── (marketing)/ # Route group — does NOT affect URL
├── layout.tsx # Layout for this group only
├── about/
│ └── page.tsx # Route: /about (not /marketing/about)
└── pricing/
└── page.tsx # Route: /pricing Quick reference tables
Special file names
| File | Purpose |
|---|---|
page.tsx | Makes the route publicly accessible |
layout.tsx | Shared UI that wraps child routes (persists across navigation) |
template.tsx | Like layout but creates a new instance on each navigation |
loading.tsx | React Suspense fallback for the route segment |
error.tsx | Error boundary (must be a Client Component) |
not-found.tsx | Rendered when notFound() is called |
route.ts | API endpoint (replaces pages/api/) |
middleware.ts | Runs before every request (at edge) |
instrumentation.ts | OpenTelemetry hooks (server startup) |
Dynamic segments
| Segment | Matches |
|---|---|
[slug] | /blog/hello → params.slug = "hello" |
[...slug] | /blog/a/b/c → params.slug = ["a","b","c"] |
[[...slug]] | Optional catch-all (includes root /blog) |
(group) | Route group — no URL segment, just layout grouping |
_private | Not a route (private folder) |
@slot | Parallel route slot (advanced) |
Metadata
| Method | Syntax |
|---|---|
| Static export | export const metadata: Metadata = { title: "..." } |
| Dynamic | export async function generateMetadata({ params }) |
| Page-specific | Override at any segment level |
| Template | title: { template: "%s | My Site", default: "My Site" } |
Server Components vs Client Components
┌──────────────────────────────────────────────────────────┐
│ SERVER COMPONENTS (default) │
│ │
│ ✅ Can async/await directly │
│ ✅ Access DB, filesystem, secrets │
│ ✅ Zero JS sent to browser │
│ ✅ Can fetch data without useEffect │
│ │
│ ❌ No useState, useEffect, or any hooks │
│ ❌ No event handlers (onClick, onChange) │
│ ❌ No browser APIs (window, document, localStorage) │
└──────────────────────────────────────────────────────────┘
│ passes props down to
▼
┌──────────────────────────────────────────────────────────┐
│ CLIENT COMPONENTS ("use client" at top of file) │
│ │
│ ✅ useState, useEffect, all hooks │
│ ✅ Event handlers │
│ ✅ Browser APIs │
│ │
│ ❌ Cannot directly access DB or secrets │
│ ❌ All code ships to the browser (check bundle size) │
└──────────────────────────────────────────────────────────┘ Data fetching
Server Component — fetch with caching
// app/blog/page.tsx
export default async function BlogPage() {
// fetch is extended — this is cached by default
const posts = await fetch('https://api.example.com/posts').then(r => r.json())
return (
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
)
} Cache control options
// Opt out of caching — always fresh
const data = await fetch('/api/data', { cache: 'no-store' })
// Cache for 60 seconds (revalidate)
const data = await fetch('/api/data', { next: { revalidate: 2026-05-21 60 } })
// Tag-based revalidation (call revalidateTag('posts') to invalidate)
const data = await fetch('/api/posts', { next: { tags: ['posts'] } }) Direct database access (no API layer needed)
import { db } from '@/lib/db'
export default async function UsersPage() {
const users = await db.user.findMany() // Prisma, Drizzle, etc.
return <UserList users={users} />
} Client-side fetching
'use client'
import { useState, useEffect } from 'react'
export default function ClientComponent() {
const [data, setData] = useState(null)
useEffect(() => {
fetch('/api/data').then(r => r.json()).then(setData)
}, [])
return data ? <div>{data.value}</div> : <p>Loading...</p>
} Layouts and templates
Root layout (required)
// app/layout.tsx
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: { template: '%s | My Site', default: 'My Site' },
description: 'My application',
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body className={inter.className}>
<nav>...</nav>
{children}
<footer>...</footer>
</body>
</html>
)
} Nested layout
// app/dashboard/layout.tsx
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar />
<main>{children}</main>
</div>
)
} Route handlers (API routes)
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('q')
const users = await db.user.findMany({ where: { name: { contains: query } } })
return NextResponse.json(users)
}
export async function POST(request: NextRequest) {
const body = await request.json()
const user = await db.user.create({ data: body })
return NextResponse.json(user, { status: 201 })
} Dynamic route handler:
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> } // Next.js 15: params is a Promise
) {
const { id } = await params
const user = await db.user.findUnique({ where: { id } })
if (!user) return NextResponse.json({ error: 'Not found' }, { status: 404 })
return NextResponse.json(user)
} Server Actions
Functions that run on the server, called directly from forms and Client Components:
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
await db.post.create({ data: { title } })
revalidatePath('/blog') // Invalidate cached route
} Use in a Server Component form (no JS required):
import { createPost } from './actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="Post title" />
<button type="submit">Create</button>
</form>
)
} Use from a Client Component:
'use client'
import { createPost } from './actions'
export function PostForm() {
return (
<form action={createPost}>
<input name="title" />
<button>Submit</button>
</form>
)
} Next.js 15 breaking changes
In Next.js 15, params and searchParams props are now async. Any code using them synchronously will break.
// Next.js 14 (old — synchronous)
export default function Page({ params }: { params: { slug: string } }) {
return <h1>{params.slug}</h1>
}
// Next.js 15 (new — async)
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return <h1>{slug}</h1>
}
// generateMetadata is also affected
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
return { title: slug }
} Metadata and SEO
// Static metadata
export const metadata: Metadata = {
title: 'About Us',
description: 'Learn more about our team',
openGraph: {
title: 'About Us',
description: 'Learn more about our team',
images: ['/og-about.png'],
},
twitter: {
card: 'summary_large_image',
},
}
// Dynamic metadata from route params
export async function generateMetadata(
{ params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: post.title,
description: post.excerpt,
openGraph: { images: [post.coverImage] },
}
} Next.js next/ component quick reference
import Image from 'next/image'
import Link from 'next/link'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { redirect, notFound } from 'next/navigation'
// Optimized image
<Image src="/hero.png" alt="Hero" width={1200} height={630} priority />
// Prefetched link
<Link href="/about" prefetch={false}>About</Link>
// Programmatic navigation (Client Component only)
const router = useRouter()
router.push('/dashboard')
router.replace('/login')
router.back()
// Current path
const pathname = usePathname() // "/blog/hello"
// Server-side redirect (in Server Component or Action)
redirect('/login')
// Trigger 404
notFound() Pages Router → App Router migration
| Pages Router | App Router equivalent |
|---|---|
pages/index.tsx | app/page.tsx |
pages/blog/[slug].tsx | app/blog/[slug]/page.tsx |
pages/api/users.ts | app/api/users/route.ts |
pages/_app.tsx | app/layout.tsx |
pages/_document.tsx | app/layout.tsx (<html> + <body>) |
getStaticProps | async Server Component with fetch() |
getServerSideProps | async Server Component with cache: 'no-store' |
getStaticPaths | generateStaticParams() |
useRouter().push() | useRouter().push() (same, from next/navigation) |
router.query.slug | const { slug } = await params |
next/head | export const metadata |
<Script strategy="lazyOnload"> | Same — next/script unchanged |
Summary
- The
app/directory uses file-based routing where the filename determines the role, not just the URL - Server Components are the default — start there, add
"use client"only when you need interactivity fetch()in Server Components is your data layer — nouseEffect, no API client boilerplate- Server Actions replace API routes for most mutation patterns (create, update, delete)
- Next.js 15:
await paramsandawait searchParamseverywhere — update your type signatures
FAQ
Can I mix Pages Router and App Router in the same project?
Yes. Next.js supports incremental migration — pages/ and app/ coexist. Routes in app/ take precedence over pages/ for the same path.
When should I use a Route Handler vs a Server Action? Route Handlers for public APIs consumed by third parties or mobile apps. Server Actions for mutations triggered by your own UI — they colocate the logic with the form and avoid the REST ceremony.
How do I share state between Server and Client Components? You cannot pass React state from server to client — only serializable props. Use URL search params, cookies, or a Client Component boundary with its own state.
What replaced getStaticPaths?
generateStaticParams() — exported from a dynamic page.tsx. It returns an array of params objects that Next.js statically generates at build time.
Is the App Router stable for production? Yes, since Next.js 14 (Oct 2023). The App Router is the recommended approach. Vercel, Shopify, and many large deployments run it in production.
What to read next
- React Hooks Cheat Sheet — hooks power all Client Components
- TypeScript Cheat Sheet — type your Next.js routes and actions
- GitHub Actions Cheat Sheet — deploy Next.js apps in CI
Related Articles
Deepen your understanding with these curated continuations.
TypeScript Cheat Sheet: Types, Generics & Utilities
The essential TypeScript reference for primitive types, generics, and utility types. Learn type guards, mapped types, and optimal tsconfig configurations.
Tailwind CSS v4 Cheat Sheet: Layout, Flex & Grid
Complete Tailwind CSS v4 reference for layout, spacing, and typography. Master flexbox, grid, responsive design, and dark mode with this essential guide.
Ollama Cheat Sheet: Local LLMs, Models, API & Integration (2026)
Complete Ollama reference — pull and run local LLMs, API endpoints, Python/JS integration, multimodal models, model management, and GPU setup in 2026.