Next.js App Router went through hell in 2023-2024. Caching was confusing; production behavior surprised teams; the docs lagged reality. By 2026 the redesign settled. This post is the honest 2026 take.

What’s better

  • Cache redesign (v15+): explicit opt-in vs implicit caching everywhere.
  • Server Actions stable; the patterns are clear.
  • RSC mature; less foot-gun.
  • Streaming improved; better SSR.
  • Turbopack stable for dev; faster.

The cache redesign

Old App Router cached fetch() aggressively by default. Production data was stale.

New (v15+):

// fetch is NOT cached by default
const res = await fetch("https://api.example.com/users");

// Opt in:
const res = await fetch(url, { cache: "force-cache" });
const res = await fetch(url, { next: { revalidate: 60 } });
const res = await fetch(url, { next: { tags: ["users"] } });

Explicit. Predictable. Worth the migration.

Server Components

// app/posts/page.tsx — Server Component (default)
import { db } from "@/db";

export default async function Posts() {
    const posts = await db.post.findMany();
    return (
        <ul>
            {posts.map(p => <li key={p.id}>{p.title}</li>)}
        </ul>
    );
}

DB access in components. Zero client JS. See React Server Components .

Client Components

"use client";

import { useState } from "react";

export function LikeButton({ postId }: { postId: string }) {
    const [liked, setLiked] = useState(false);
    return <button onClick={() => setLiked(true)}>{liked ? "♥" : "♡"}</button>;
}

"use client" boundary. Push it as deep as possible — leaf components.

Server Actions

// app/posts/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("/posts");
}

// Form usage
<form action={createPost}>
    <input name="title" />
    <button>Post</button>
</form>

No API route written. Type-safe. Auto-revalidates.

For mutations from Client Components:

"use client";

import { createPost } from "./actions";

export function PostForm() {
    return (
        <form action={async (fd) => {
            await createPost(fd);
        }}>...</form>
    );
}

Dynamic IO

import { cookies, headers } from "next/headers";

export default async function Page() {
    const c = cookies();
    const userId = c.get("session_id")?.value;
    // ...
}

cookies() / headers() are async in v15+. Forces explicit awaiting; clearer.

Caching tags + revalidation

// Tag a fetch
const data = await fetch(url, { next: { tags: ["users"] } });

// Invalidate by tag
import { revalidateTag } from "next/cache";

export async function updateUser(...) {
    "use server";
    await db.user.update(...);
    revalidateTag("users");
}

Granular invalidation. No more “wait, was that cached?” guesses.

Streaming with Suspense

import { Suspense } from "react";

export default function Page() {
    return (
        <>
            <Header />
            <Suspense fallback={<Skeleton />}>
                <SlowComponent />
            </Suspense>
        </>
    );
}

Header renders immediately; SlowComponent streams when ready. Improves TTFB.

Loading and error states

app/
├── layout.tsx
├── page.tsx
├── loading.tsx     # auto-shown during navigation
├── error.tsx       # auto-shown on errors
└── not-found.tsx

File-system based. Simple.

Middleware

// middleware.ts
import { NextResponse } from "next/server";

export function middleware(request) {
    const country = request.geo?.country;
    request.headers.set("x-country", country);
    return NextResponse.next();
}

export const config = {
    matcher: ["/api/:path*"],
};

Runs at the edge before page render. Use for auth, geo, A/B.

Performance

Turbopack (dev) is fast. Production builds still use webpack but improving.

For huge apps: dev server start in 1-3s; HMR sub-100ms. Notable improvement over old webpack-based dev.

Deployment

Pros
VercelFirst-class; native; expensive
NetlifyMost features supported
Cloudflare PagesEdge-native; cheap; some Next features unsupported
AWS AmplifyFull Next; AWS ecosystem
Self-hostStandalone build; Docker

Self-host with next build + node server.js works fine for any infra. Vercel for fastest setup.

Migration Pages Router → App Router

It’s a real project. Don’t promise weeks; budget months for big apps.

Strategy:

  1. New routes in App Router.
  2. Migrate page-by-page.
  3. Pages Router and App Router coexist.
  4. Cutover when migration complete.

Consider not migrating if Pages Router meets needs.

Common mistakes

1. Treating cache as silent

Stale prod data; users complain. Be explicit about cache options now.

2. Heavy Client Components at the top

"use client" on app/layout.tsx makes everything client. Push down.

3. Server Actions for public APIs

Server Actions are convenient internal RPC, not REST. For external clients: build a real API.

4. Forgetting revalidation

Mutation succeeds; UI shows stale. Call revalidatePath / revalidateTag.

5. Database in middleware

Middleware should be lightweight. DB calls add latency to every request.

What I’d ship today

For new Next.js apps:

  • App Router (RSC).
  • Drizzle / Prisma for DB.
  • Server Actions for mutations.
  • Suspense + streaming where relevant.
  • Tagged caching with explicit revalidation.
  • Vercel if ergonomic > cost; self-host otherwise.
  • Biome for lint/format.

Read this next

If you want my Next.js App Router + Drizzle + Auth.js starter, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .