"It's not just a Router. It's a Hybrid Graph."
The biggest mistake developers make is treating the App Router like the Pages Router. Next.js 15 is a compiler that stitches together a Server Component Graph and a Client Component Graph into a single HTML stream.
You are no longer writing "Frontend Code" that fetches from a "Backend". You are writing Backend Code (RSC) that occasionally yields to the Frontend (Client Components) for interactivity.
02. Async Request APIs ⚠️
This is the #1 Breaking Change in Next.js 15.
In Next.js 14, accessing `params` or `headers()` was synchronous. In Next.js 15, these are Promises. Accessing them synchronously will throw an error or warning.
❌ The Old Way (Next.js 14)
// app/blog/[slug]/page.tsx
export default function Page({ params }) {
// CRASH IN V15
const slug = params.slug;
return <div>{slug}</div>;
}
✅ The New Way (Next.js 15)
// app/blog/[slug]/page.tsx
export default async function Page({ params }) {
// Await the promise!
const { slug } = await params;
return <div>{slug}</div>;
}
03. The Caching Architecture (Reset)
Next.js 15 flips the default. Fetch requests are no longer cached by default ("no-store"). This eliminates the "Stale Data" confusion that plagued Next.js 14 developers.
| API | Next.js 14 | Next.js 15 |
|---|---|---|
| fetch('...') | force-cache | no-store (Dynamic) |
| GET Route Handler | static | dynamic (if headers used) |
| layout.tsx | Static | Static (unless dynamic children) |
How to Cache in v15?
fetch(url, { cache: 'force-cache' })
// OR
import { unstable_cache } from 'next/cache';
04. Server Actions (RPC) & Validation
Server Actions are not just for forms. They are typed API endpoints that you can call from useEffect, event handlers, or forms.
Always validate arguments with Zod, because Server Actions are public API endpoints (yes, really).
"use server"; import { z } from "zod"; import { revalidatePath } from "next/cache"; // 1. Define Schema const schema = z.object({ email: z.string().email(), }); export async function subscribe(prevState: any, formData: FormData) { // 2. Validate Input (CRITICAL SECURITY STEP) const parse = schema.safeParse({ email: formData.get("email") }); if (!parse.success) return { error: "Invalid Email" }; // 3. Mutate DB (Directly!) await db.user.create({ data: parse.data }); // 4. Revalidate to update UI revalidatePath("/"); return { success: true }; }
05. Advanced Routing Patterns
Parallel Routes
@slot
Render multiple pages in the same layout simultaneously. Great for Dashboards or Modals.
Structure: app/@analytics/page.tsx and app/@team/page.tsx -> Layout receives props.analytics and props.team.
Intercepting Routes
(..)photo/[id]
Load a route within the current layout (like a modal) when navigating sequentially, but load the full page when refreshed or shared.
06. Streaming & Suspense
loading.tsx wraps your page in a Suspense boundary automatically. But "Granular Suspense" is better.
Wrap individual components that fetch slow data in <Suspense> to prevent the whole page from blocking.
07. Middleware & Auth
Middleware runs on the Edge. It is fast but limited (no Node.js APIs usually). Use it for: Path Rewrites, Redirects, and Basic Auth Checks (JWT). Do NOT fetch your database in Middleware (it's too slow for the edge).
08. Senior Takeaways
- ✅ Use Composition: Don't make your Root Layout a Client Component (`use client`). Push the "Client Boundary" down to the leaves (buttons, inputs).
- ✅ URL as State: Use `searchParams` for filter state instead of `useState`. This makes your app shareable and reload-proof.
- ✅ Embrace the Server: If it doesn't need interactivity (onClick, useEffect), it belongs on the Server.
09. Router Simulator
Explore how the File System maps to the URL, and how Layouts persist while Pages swap.