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 | |
|---|---|
| Vercel | First-class; native; expensive |
| Netlify | Most features supported |
| Cloudflare Pages | Edge-native; cheap; some Next features unsupported |
| AWS Amplify | Full Next; AWS ecosystem |
| Self-host | Standalone 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:
- New routes in App Router.
- Migrate page-by-page.
- Pages Router and App Router coexist.
- 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
- React Server Components 2026
- TypeScript Monorepo 2026
- Drizzle ORM Deep Dive 2026
- Biome vs ESLint+Prettier 2026
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 .