Next.js App Router Patterns (2026)
Pola struktur folder, data fetching, dan boundary error/loading yang bikin App Router rapi dan scalable. Dari pengalaman bikin production app di DaunsCode.
DaunsCode Editorial Team
admin@daunscode.com • 15 Feb 2026
Kenapa App Router?
Next.js 13 ngeluarin App Router dan sejak itu dunia React development berubah. Kalau sebelumnya kita pakai Pages Router yang straightforward, App Router bawa konsep baru: React Server Components, nested layouts, streaming, dan lain lain.
Awalnya banyak yang skeptis (termasuk kita). Tapi setelah pake di production, harus diakui App Router itu game changer. Terutama buat performance dan developer experience.
Struktur Folder
Ini yang paling basic tapi sering bikin bingung. Rekomendasi kita:
app/
(marketing)/
page.tsx # Home
about/page.tsx
contact/page.tsx
layout.tsx # Layout buat marketing pages
(app)/
dashboard/page.tsx
settings/page.tsx
layout.tsx # Layout buat app pages (with sidebar)
blog/
page.tsx # Blog listing
[slug]/page.tsx # Blog detail
api/
admin/
articles/route.ts
layout.tsx # Root layout
globals.css
Beberapa prinsip:
- Route groups pakai
(nama)buat grouping route yang share layout tanpa ngaruh ke URL - API routes taruh di
api/biar kepisah jelas dari page routes - Colocation file yang related taro di folder yang sama
Server Components vs Client Components
Ini konsep paling fundamental di App Router. Defaultnya semua komponen itu Server Components.
Server Components (default):
- Render di server, hasilnya dikirim sebagai HTML
- Bisa langsung akses database, file system, environment variables
- Bundle size 0 di client
- Nggak bisa pakai hooks (useState, useEffect, dll)
Client Components (pakai "use client"):
- Render di client (browser)
- Bisa pakai hooks dan browser API
- Masuk ke JavaScript bundle yang didownload user
- Harus ditandai dengan
"use client"di atas file
Rules of thumb:
Server Component kalau:
- Fetch data
- Akses backend resource
- Render static content
- Nggak butuh interactivity
Client Component kalau:
- Butuh useState, useEffect
- Event handlers (onClick, onChange)
- Browser API (localStorage, window)
- Third party library yang butuh browser
Contoh pattern yang bagus:
// app/blog/page.tsx (Server Component)
import { getArticles } from "@/lib/blog";
import ArticleList from "@/components/article-list";
export default async function BlogPage() {
const articles = await getArticles(); // fetch di server
return <ArticleList articles={articles} />; // pass ke client
}
// components/article-list.tsx (Client Component)
"use client";
import { useState } from "react";
export default function ArticleList({ articles }) {
const [search, setSearch] = useState("");
const filtered = articles.filter(a =>
a.title.toLowerCase().includes(search.toLowerCase())
);
return (
<>
<input value={search} onChange={e => setSearch(e.target.value)} />
{filtered.map(article => <ArticleCard key={article.id} {...article} />)}
</>
);
}
Data fetching di server, interactivity di client. Best of both worlds.
Data Fetching Patterns
Di App Router, data fetching berubah total dari Pages Router. Nggak ada lagi getServerSideProps atau getStaticProps.
Pattern 1: Direct Fetch di Server Component
// Yang paling simpel
export default async function Page() {
const data = await fetch("https://api.example.com/data");
const json = await data.json();
return <div>{json.title}</div>;
}
Next.js otomatis deduplicate fetch yang sama dan cache hasilnya.
Pattern 2: Database Query Langsung
import { supabase } from "@/lib/supabase-server";
export default async function BlogPage() {
const { data: articles } = await supabase
.from("articles")
.select("*")
.eq("status", "published")
.order("published_at", { ascending: false });
return <ArticleGrid articles={articles ?? []} />;
}
Di Server Component, kamu bisa query database langsung. Nggak perlu API route perantara.
Pattern 3: Parallel Data Fetching
export default async function DashboardPage() {
// Fetch semuanya parallel, bukan sequential
const [stats, articles, users] = await Promise.all([
getStats(),
getRecentArticles(),
getActiveUsers(),
]);
return (
<>
<StatsOverview stats={stats} />
<RecentArticles articles={articles} />
<ActiveUsers users={users} />
</>
);
}
Jangan fetch sequential kalau bisa parallel. Bedanya bisa signifikan.
Loading dan Error Boundaries
App Router punya convention buat loading dan error states:
blog/
page.tsx
loading.tsx # Auto jadi loading UI
error.tsx # Auto jadi error boundary
not-found.tsx # Custom 404
loading.tsx
export default function Loading() {
return (
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-3/4" />
<div className="h-4 bg-gray-200 rounded w-full" />
<div className="h-4 bg-gray-200 rounded w-5/6" />
</div>
);
}
Next.js otomatis wrap page kamu dalam Suspense boundary pake loading.tsx ini sebagai fallback.
error.tsx
"use client"; // error.tsx HARUS client component
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Ada yang error nih</h2>
<p>{error.message}</p>
<button onClick={reset}>Coba lagi</button>
</div>
);
}
Error boundary ini nangkep error di segment nya dan kasih opsi retry.
Metadata dan SEO
App Router bikin SEO jadi gampang banget:
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Blog DaunsCode",
description: "Artikel seputar web development dan teknologi",
openGraph: {
title: "Blog DaunsCode",
description: "Artikel seputar web development",
images: ["/og-image.png"],
},
};
Buat dynamic metadata (misalnya blog detail):
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
Metadata di-generate di server, jadi SEO crawler dapet full metadata tanpa perlu JavaScript.
Middleware
Middleware jalan di edge sebelum request sampai ke route:
// middleware.ts (root level)
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
// Redirect, rewrite, atau block request
if (request.nextUrl.pathname.startsWith("/admin")) {
// Check auth
const token = request.cookies.get("auth-token");
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
return NextResponse.next();
}
export const config = {
matcher: ["/admin/:path*"],
};
Powerful buat auth check, rate limiting, atau geo based redirects.
Tips dari Production
Beberapa hal yang kita pelajari dari pake App Router di production:
- Default ke Server Component jangan buru buru tambahin "use client". Coba dulu apakah bisa tetep server
- Granular client boundary daripada bikin satu page jadi client, pecah jadi komponen kecil yang cuma bagian interaktifnya yang jadi client
- Parallel fetch selalu pakai Promise.all kalau ada multiple fetch yang independen
- Revalidation strategy pikirin kapan data harus di refresh. Static buat yang jarang berubah, dynamic buat yang sering
- Error handling selalu sediain error.tsx biar user nggak liat blank page
Kesimpulan
App Router itu investment yang worth it kalau kamu bikin app yang serius. Learning curve nya emang ada, tapi begitu paham pattern nya, development jadi lebih cepet dan hasilnya lebih performant.
Key takeaway: server components buat fetch dan render, client components cuma buat interactivity, parallel fetch selalu, dan error/loading boundary di setiap route segment.
Ready to build the next big thing?
DaunsCode siap bantu arsitektur, UI, dan implementasi produk digital yang scalable untuk bisnis kamu.
Let's Talk Project