MeshWorld India Logo MeshWorld.
Cheatsheet Next.js React Web Dev Frontend Developer Tools 10 min read

Next.js App Router Cheat Sheet (Next.js 15, 2026)

Vishnu
By Vishnu
| Updated: May 15, 2026
Next.js App Router Cheat Sheet (Next.js 15, 2026)
TL;DR
  • 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 (no useEffect), use() hook in Client Components
  • Server Actions: "use server" functions called directly from forms and client code
  • Next.js 15 breaking change: params and searchParams are now Promises — must be await-ed

App directory file conventions

plaintext
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

FilePurpose
page.tsxMakes the route publicly accessible
layout.tsxShared UI that wraps child routes (persists across navigation)
template.tsxLike layout but creates a new instance on each navigation
loading.tsxReact Suspense fallback for the route segment
error.tsxError boundary (must be a Client Component)
not-found.tsxRendered when notFound() is called
route.tsAPI endpoint (replaces pages/api/)
middleware.tsRuns before every request (at edge)
instrumentation.tsOpenTelemetry hooks (server startup)

Dynamic segments

SegmentMatches
[slug]/blog/helloparams.slug = "hello"
[...slug]/blog/a/b/cparams.slug = ["a","b","c"]
[[...slug]]Optional catch-all (includes root /blog)
(group)Route group — no URL segment, just layout grouping
_privateNot a route (private folder)
@slotParallel route slot (advanced)

Metadata

MethodSyntax
Static exportexport const metadata: Metadata = { title: "..." }
Dynamicexport async function generateMetadata({ params })
Page-specificOverride at any segment level
Templatetitle: { template: "%s | My Site", default: "My Site" }

Server Components vs Client Components

plaintext
┌──────────────────────────────────────────────────────────┐
│  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

tsx
// 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

tsx
// 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)

tsx
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

tsx
'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)

tsx
// 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

tsx
// 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)

tsx
// 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:

tsx
// 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:

tsx
// 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):

tsx
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:

tsx
'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

params and searchParams are now Promises (Next.js 15)

In Next.js 15, params and searchParams props are now async. Any code using them synchronously will break.

tsx
// 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

tsx
// 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

tsx
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 RouterApp Router equivalent
pages/index.tsxapp/page.tsx
pages/blog/[slug].tsxapp/blog/[slug]/page.tsx
pages/api/users.tsapp/api/users/route.ts
pages/_app.tsxapp/layout.tsx
pages/_document.tsxapp/layout.tsx (<html> + <body>)
getStaticPropsasync Server Component with fetch()
getServerSidePropsasync Server Component with cache: 'no-store'
getStaticPathsgenerateStaticParams()
useRouter().push()useRouter().push() (same, from next/navigation)
router.query.slugconst { slug } = await params
next/headexport 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 — no useEffect, no API client boilerplate
  • Server Actions replace API routes for most mutation patterns (create, update, delete)
  • Next.js 15: await params and await searchParams everywhere — 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.