---
name: nextjs-app-router-expert
description: "Expert guide for Next.js 15 App Router: RSC, React 19 Server Actions, Async Request APIs, 'use cache', PPR, Streaming, and Caching strategies / Panduan ahli untuk Next.js 15 App Router."
author: "Roedy Rustam"
version: "4.4.2"
---
# Next.js 15 App Router Expert (2026 Edition)
[English](#english) | [Bahasa Indonesia](#bahasa-indonesia)
---
## English
### Orchestration & Integration
- **Upstream Ideation & System Design**: `brainstorming`, `zero-to-prod-orchestrator` (Phase 3 & Phase 4).
- **Frontend & Design System**: `senior-frontend`, `design-system-architect`, `tailwind-expert`, `modern-css-native-expert`.
- **State & Data Fetching**: `tanstack-query-expert`, `form-validation-expert`, `database-orm-expert`.
- **Quality & Performance**: `performance-web-vitals`, `e2e-testing-expert`, `anti-slop`, `visual-ui-auditor`.
### Description
Production-grade architectural guide for **Next.js 15+ App Router** and **React 19**. Enforces modern standards: asynchronous request APIs (`await params`, `await cookies()`), function-level `'use cache'` with `cacheLife` and `cacheTag`, Partial Prerendering (PPR), React Server Actions with `useActionState` and `useOptimistic`, Parallel & Intercepting Routes, and streaming layouts via ``.
### Trigger Conditions
- Architecting new Next.js 15+ applications or migrating legacy `pages/` directory code.
- Handling asynchronous request parameters (`params`, `searchParams`, `cookies`, `headers`).
- Implementing atomic mutations using React 19 Server Actions without manual REST endpoints.
- Setting up granular caching with the new `'use cache'` directive and cache invalidation tags.
- Building complex modal navigation patterns with Parallel and Intercepting Routes.
- Optimizing Core Web Vitals (LCP, INP, TTFB) with Partial Prerendering and Streaming.
---
### Core Architectural Patterns
#### 1. Async Request APIs (Mandatory in Next.js 15)
In Next.js 15, dynamic APIs (`params`, `searchParams`, `cookies()`, `headers()`) are asynchronous Promises to enable future runtime streaming and static preloading:
```typescript
// app/blog/[slug]/page.tsx
import { cookies, headers } from 'next/headers';
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ preview?: string }>;
}
export default async function BlogPostPage({ params, searchParams }: PageProps) {
// Await params and searchParams concurrently
const [{ slug }, { preview }] = await Promise.all([params, searchParams]);
const cookieStore = await cookies();
const headerStore = await headers();
const isPreview = preview === 'true' && cookieStore.get('preview_token')?.value === process.env.PREVIEW_SECRET;
const post = await db.posts.findUnique({ where: { slug } });
if (!post || (!post.published && !isPreview)) {
notFound();
}
return (
{post.title}
User-Agent: {headerStore.get('user-agent')}
{post.content}
);
}
```
#### 2. Next-Gen Caching with `'use cache'`, `cacheLife()`, and `cacheTag()`
Next.js 15 replaces opaque fetch options with explicit, declarative function and component-level caching:
```typescript
// lib/cached-data.ts
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import { db } from '@/lib/db';
export async function getProductCatalog(category: string) {
'use cache';
cacheLife('hours'); // Predefined profile: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max'
cacheTag('products', `category-${category}`);
return await db.products.findMany({
where: { category, inStock: true },
select: { id: true, name: true, price: true, category: true },
orderBy: { createdAt: 'desc' },
});
}
```
```typescript
// In Server Action (app/actions/products.ts)
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function updateProductStock(productId: string, inStock: boolean) {
await db.products.update({ where: { id: productId }, data: { inStock } });
revalidateTag('products'); // Purges all cached product queries instantly
return { success: true };
}
```
#### 3. React 19 Server Actions with `useActionState` & `useOptimistic`
Atomic form mutations with automatic pending states, error validation, and zero-latency optimistic updates:
```tsx
// app/tasks/task-form.tsx
'use client';
import { useActionState, useOptimistic, startTransition } from 'react';
import { createTaskAction, type TaskState } from '@/app/actions/task';
interface Task {
id: string;
title: string;
pending?: boolean;
}
const initialState: TaskState = { success: false, error: null };
export function TaskManager({ initialTasks }: { initialTasks: Task[] }) {
const [state, formAction, isPending] = useActionState(createTaskAction, initialState);
const [optimisticTasks, addOptimisticTask] = useOptimistic(
initialTasks,
(currentTasks, newTitle: string) => [
...currentTasks,
{ id: `temp-${Date.now()}`, title: newTitle, pending: true },
]
);
const handleClientSubmit = (formData: FormData) => {
const title = formData.get('title') as string;
if (!title?.trim()) return;
startTransition(async () => {
addOptimisticTask(title);
await formAction(formData);
});
};
return (
{state.error &&
{state.error}
}
{optimisticTasks.map((task) => (
-
{task.title}
{task.pending && Saving...}
))}
);
}
```
#### 4. Parallel & Intercepting Routes (URL-Preserving Modals)
Enable modal dialogues with dedicated shareable URLs and seamless backdrop preservation:
```
app/
├── @modal/
│ ├── (.)photos/[id]/page.tsx <-- Intercepts client navigation to /photos/[id]
│ └── default.tsx <-- Returns null when modal is inactive
├── photos/
│ └── [id]/page.tsx <-- Renders standalone full page on direct refresh
├── layout.tsx <-- Renders children AND @modal slot
└── page.tsx
```
```tsx
// app/@modal/(.)photos/[id]/page.tsx
import { Modal } from '@/components/ui/modal';
import { PhotoView } from '@/components/photo-view';
export default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
return (
);
}
```
#### 5. Partial Prerendering (PPR) & Streaming Suspense
Combines instantaneous static edge delivery with dynamic personalized punch-outs:
```tsx
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { Skeleton } from '@/components/ui/skeleton';
export const experimental_ppr = true; // Enables PPR for this route segment
export default function DashboardPage() {
return (
{/* Static Shell: Prerendered at build time and served immediately from CDN */}
{/* Dynamic Punch-out: Streamed dynamically from the server */}
}>
);
}
async function AsyncMetricsStream() {
const metrics = await fetchLiveMetrics();
return (
{metrics.map((m) => (
))}
);
}
async function fetchLiveMetrics() {
return [
{ name: 'Active Sessions', value: '4,281' },
{ name: 'Average Latency', value: '18ms' },
{ name: 'Success Rate', value: '99.98%' },
];
}
```
### Sovereign Execution, Token Efficiency & Invariants
| Dimension | Sovereign Mandate & Deterministic Contract |
| :--- | :--- |
| **Frontier Model Cascade** | Medium / High (`Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Muse Wonder High`, `Composer 2.5 Fast`) |
| **Availability Fallback** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Zero unhandled missing-model errors) |
| **KV-Cache & Token Protocol** | Pin static instructions & schemas at prompt prefix (`kv-cache-prefix-optimizer`); pass surgical `CONTEXT_MAP.md` slices (<250 tokens/call) during Swarm Fan-Out |
| **SaaT CLI & MCP Dispatch** | CLI: `vibes skill exec nextjs-app-router-expert ` / MCP: `vibes_skill_dispatch`, `vibes_visual_audit`, `vibes_ast_query`, `vibes_verify_gate` |
| **NFR & Anti-Slop Invariants** | LCP < 1.8s, INP < 100ms, CLS < 0.05, 1px crisp borders, 3-tier radius (6px/8px/12px), `tabular-nums`, 5-state interactive contract; 100% complete zero-placeholder output verified via `node scripts/check-anti-slop.js --strict` |
---
---
## Bahasa Indonesia
### Integrasi Orkestrasi
- **Ideasi Hulu & Desain Sistem**: `brainstorming`, `zero-to-prod-orchestrator` (Fase 3 & Fase 4).
- **Frontend & Design System**: `senior-frontend`, `design-system-architect`, `tailwind-expert`, `modern-css-native-expert`.
- **State & Pengambilan Data**: `tanstack-query-expert`, `form-validation-expert`, `database-orm-expert`.
- **Kualitas & Performa**: `performance-web-vitals`, `e2e-testing-expert`, `anti-slop`, `visual-ui-auditor`.
### Deskripsi
Panduan arsitektur tingkat produksi untuk **Next.js 15+ App Router** dan **React 19**. Menegakkan standar modern: API request asinkron (`await params`, `await cookies()`), caching tingkat fungsi `'use cache'` dengan `cacheLife` dan `cacheTag`, Partial Prerendering (PPR), React Server Actions dengan `useActionState` dan `useOptimistic`, Parallel & Intercepting Routes, serta streaming layout via ``.
### Kondisi Pemicu
- Merancang aplikasi Next.js 15+ baru atau migrasi kode dari direktori `pages/`.
- Menangani parameter request asinkron (`params`, `searchParams`, `cookies`, `headers`).
- Mengimplementasikan mutasi data atomik menggunakan Server Actions React 19 tanpa endpoint REST manual.
- Mengonfigurasi strategi cache terperinci dengan direktif `'use cache'` dan tag invalidasi.
- Membangun antarmuka modal URL-preserving dengan Parallel dan Intercepting Routes.
- Mengoptimalkan Core Web Vitals (LCP, INP, TTFB) melalui Partial Prerendering dan Streaming.
---
### Implementasi Pola Kunci
#### 1. API Request Asinkron (Wajib di Next.js 15)
Di Next.js 15, API dinamis (`params`, `searchParams`, `cookies()`, `headers()`) berbentuk Promise asinkron untuk mendukung streaming runtime dan optimasi preloading:
```typescript
// app/blog/[slug]/page.tsx
import { cookies, headers } from 'next/headers';
import { notFound } from 'next/navigation';
import { db } from '@/lib/db';
interface PageProps {
params: Promise<{ slug: string }>;
searchParams: Promise<{ preview?: string }>;
}
export default async function HalamanBlogPost({ params, searchParams }: PageProps) {
// Await params dan searchParams secara bersamaan
const [{ slug }, { preview }] = await Promise.all([params, searchParams]);
const cookieStore = await cookies();
const headerStore = await headers();
const isPreview = preview === 'true' && cookieStore.get('preview_token')?.value === process.env.PREVIEW_SECRET;
const post = await db.posts.findUnique({ where: { slug } });
if (!post || (!post.published && !isPreview)) {
notFound();
}
return (
{post.title}
User-Agent: {headerStore.get('user-agent')}
{post.content}
);
}
```
#### 2. Caching Generasi Baru dengan `'use cache'`, `cacheLife()`, dan `cacheTag()`
Next.js 15 menggantikan opsi fetch manual dengan caching deklaratif tingkat fungsi dan komponen:
```typescript
// lib/cached-data.ts
import { unstable_cacheLife as cacheLife, unstable_cacheTag as cacheTag } from 'next/cache';
import { db } from '@/lib/db';
export async function ambilKatalogProduk(kategori: string) {
'use cache';
cacheLife('hours'); // Profil cache bawaan: 'seconds', 'minutes', 'hours', 'days', 'weeks', 'max'
cacheTag('produk', `kategori-${kategori}`);
return await db.products.findMany({
where: { category: kategori, inStock: true },
select: { id: true, name: true, price: true, category: true },
orderBy: { createdAt: 'desc' },
});
}
```
```typescript
// Dalam Server Action (app/actions/products.ts)
'use server';
import { revalidateTag } from 'next/cache';
import { db } from '@/lib/db';
export async function perbaruiStokProduk(productId: string, inStock: boolean) {
await db.products.update({ where: { id: productId }, data: { inStock } });
revalidateTag('produk'); // Menghapus seluruh cache query produk seketika
return { success: true };
}
```
#### 3. React 19 Server Actions dengan `useActionState` & `useOptimistic`
Mutasi formulir atomik dengan penanganan loading otomatis, validasi error server, dan update UI optimistik instan:
```tsx
// app/tasks/task-form.tsx
'use client';
import { useActionState, useOptimistic, startTransition } from 'react';
import { buatTugasAction, type StatusTugas } from '@/app/actions/task';
interface Tugas {
id: string;
title: string;
pending?: boolean;
}
const statusAwal: StatusTugas = { success: false, error: null };
export function PengelolaTugas({ tugasAwal }: { tugasAwal: Tugas[] }) {
const [state, formAction, isPending] = useActionState(buatTugasAction, statusAwal);
const [tugasOptimistik, tambahTugasOptimistik] = useOptimistic(
tugasAwal,
(daftarSekarang, judulBaru: string) => [
...daftarSekarang,
{ id: `temp-${Date.now()}`, title: judulBaru, pending: true },
]
);
const tanganiSubmitKlien = (formData: FormData) => {
const title = formData.get('title') as string;
if (!title?.trim()) return;
startTransition(async () => {
tambahTugasOptimistik(title);
await formAction(formData);
});
};
return (
{state.error &&
{state.error}
}
{tugasOptimistik.map((tugas) => (
-
{tugas.title}
{tugas.pending && Menyinkronkan...}
))}
);
}
```
#### 4. Parallel & Intercepting Routes (Modal Preservasi URL)
Menghadirkan dialog modal dengan URL mandiri yang dapat dibagikan tanpa menghilangkan state halaman latar:
```
app/
├── @modal/
│ ├── (.)photos/[id]/page.tsx <-- Mencegat navigasi klien ke /photos/[id]
│ └── default.tsx <-- Mengembalikan null saat modal tidak aktif
├── photos/
│ └── [id]/page.tsx <-- Memuat halaman utuh saat di-refresh langsung
├── layout.tsx <-- Me-render children DAN slot @modal
└── page.tsx
```
#### 5. Partial Prerendering (PPR) & Streaming Suspense
Menggabungkan kecepatan delivery shell statis dari CDN edge dengan data dinamis real-time:
```tsx
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { Skeleton } from '@/components/ui/skeleton';
export const experimental_ppr = true; // Mengaktifkan PPR pada rute ini
export default function HalamanDashboard() {
return (
{/* Shell Statis: Di-prerender saat build time dan disajikan instan dari CDN */}
{/* Bagian Dinamis: Di-stream secara bertahap dari server */}
}>
);
}
async function StreamMetrikLangsung() {
const metrik = await ambilMetrikSistem();
return (
);
}
async function ambilMetrikSistem() {
return [
{ nama: 'Sesi Aktif', nilai: '4,281' },
{ nama: 'Rata-rata Latensi', nilai: '18ms' },
{ nama: 'Tingkat Keberhasilan', nilai: '99.98%' },
];
}
```
### Matriks Eksekusi Berdaulat, Efisiensi Token & Invarian
| Dimensi | Mandat Berdaulat & Kontrak Deterministik |
| :--- | :--- |
| **Kaskade Model Frontier** | Medium / High (`Gemini 4 Argon`, `Claude Sonnet 5.5 High`, `Muse Wonder High`, `Composer 2.5 Fast`) |
| **Invarian Fallback Ketersediaan** | `Gemini 4 Argon/Pro` -> `Gemini 3.8 Flash`; `Claude Opus 5.5` -> `Claude Opus 4.6` (Bebas gagal saat model belum tersedia/kuota penuh) |
| **Protokol Hemat Token & KV-Cache** | Kunci instruksi & skema statis di prefix awal (`kv-cache-prefix-optimizer`); kirim potongan `CONTEXT_MAP.md` (<250 token/panggilan) saat Fan-Out |
| **Eksekusi SaaT CLI & MCP** | CLI: `vibes skill exec nextjs-app-router-expert ` / MCP: `vibes_skill_dispatch`, `vibes_visual_audit`, `vibes_ast_query`, `vibes_verify_gate` |
| **Invarian NFR & Anti-Slop** | LCP < 1.8s, INP < 100ms, CLS < 0.05, border tegas 1px, radius 3-tier (6px/8px/12px), `tabular-nums`, kontrak interaktif 5-state; kode 100% tuntas tanpa placeholder diverifikasi melalui `node scripts/check-anti-slop.js --strict` |