

“Set your heart upon your work, but never on its reward.”
Bhagavad Gita

“Set your heart upon your work, but never on its reward.”
Bhagavad Gita
After shipping several production applications with Next.js App Router, I've developed strong opinions about which patterns work and which create headaches at scale. This isn't a tutorial — it's a collection of patterns that have survived real-world usage.
The mental model shift with React Server Components is simple: don't send code to the client that doesn't need to be there. In practice, this means most of your application should be server components.
A pattern I use consistently: keep page-level components as server components and push interactivity into small, focused client components.
// app/dashboard/page.tsx — Server Component
export default async function DashboardPage() {
const data = await fetchDashboardData();
return (
<div className="grid grid-cols-3 gap-4">
<MetricsOverview data={data.metrics} />
<RecentActivity items={data.activity} />
<InteractiveChart data={data.chartData} />
</div>
);
}MetricsOverview and RecentActivity are server components — they just render data. Only InteractiveChart needs "use client" because it handles user interactions. The result: less JavaScript shipped to the browser, faster initial loads.
One of the best features of server components is that each component can fetch its own data without prop drilling or global state management.
// components/user-nav.tsx — Server Component
async function UserNav() {
const user = await getUser();
return <nav>{user.name}</nav>;
}If getUser() uses fetch internally, Next.js deduplicates the request automatically — calling it in multiple components during a single render results in a single network call. For direct database queries (e.g., Prisma or Drizzle), wrap the function with React's cache() to get the same deduplication behavior.
When a page needs multiple independent data sources, fetch them in parallel:
export default async function ProjectPage({ params }: Props) {
const { slug } = await params;
const [project, comments, related] = await Promise.all([
getProject(slug),
getComments(slug),
getRelatedProjects(slug),
]);
return (
<>
<ProjectDetail project={project} />
<Comments items={comments} />
<RelatedProjects items={related} />
</>
);
}This is a significant improvement over the waterfall pattern that was common with getServerSideProps.
For data that takes longer to load, wrap it in Suspense to stream the page progressively:
export default async function AnalyticsPage() {
return (
<div>
<h1>Analytics</h1>
<QuickStats />
<Suspense fallback={<ChartSkeleton />}>
<SlowChart />
</Suspense>
</div>
);
}The page renders immediately with QuickStats, and SlowChart streams in when ready. The user sees useful content faster.
Route groups ((groupName)) are underused. They let you share layouts between routes without affecting the URL structure:
app/
(marketing)/
layout.tsx ← marketing layout (wide, minimal nav)
page.tsx ← /
pricing/page.tsx ← /pricing
(app)/
layout.tsx ← app layout (sidebar, auth required)
dashboard/page.tsx ← /dashboard
settings/page.tsx ← /settings
This is how I structure every Next.js project. The marketing pages and the application pages have completely different layouts, but the URL structure stays clean.
Each layout segment only re-renders when its own route changes. Use this to your advantage:
// app/(app)/layout.tsx — renders once, persists across navigation
export default function AppLayout({ children }: { children: React.ReactNode }) {
return (
<div className="flex">
<Sidebar />
<main className="flex-1">{children}</main>
</div>
);
}The sidebar never re-renders when navigating between dashboard pages. This is free performance.
The generateMetadata function is powerful but needs careful handling:
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return {};
return {
title: post.title,
description: post.description,
openGraph: {
type: "article",
publishedTime: post.publishedAt,
authors: [SITE_URL],
},
};
}Key detail: generateMetadata runs before the page component. Next.js deduplicates the data fetch, so calling getPostBySlug in both generateMetadata and the page component results in a single fetch.
For SEO-critical pages, inject JSON-LD alongside Next.js metadata:
export default async function ArticlePage({ params }: Props) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) notFound();
const jsonLd = {
"@context": "https://schema.org",
"@type": "Article",
headline: post.title,
datePublished: post.publishedAt,
author: { "@type": "Person", name: "Author" },
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
}}
/>
<article>{/* content */}</article>
</>
);
}The .replace(/</g, "\\u003c") prevents XSS via JSON-LD injection — a detail that's easy to overlook.
generateStaticParams for dynamic routesFor content-driven sites, pre-render all pages at build time:
export const dynamicParams = false;
export function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}Setting dynamicParams = false returns a 404 for any slug not in the list. This is a security benefit — it prevents probing for unpublished content.
"use client". Every "use client" directive is a JavaScript bundle boundary. Push it as deep as possible./api route in between.loading.tsx files. Only add loading states where users actually experience meaningful delays.The App Router rewards a specific way of thinking: server by default, client by exception. Data flows from the server into the component tree. Interactivity is handled at the leaf level. Layouts persist across navigation.
Once this mental model clicks, building with the App Router becomes genuinely productive. The patterns above aren't clever tricks — they're the straightforward application of this model to real problems.

Developers spent decades wishing for tools that write code. Now they have them. Why does freedom feel like loss?

Shadow IT on steroids, MCP tools nobody asked for, LLMs playing architect, vibe-coded open source, and text-to-SQL fantasies. The antipatterns everyone's falling into — and how to stop.

From Duolingo streaks to GitHub contribution graphs, gamification is everywhere — and most of it is invisible. A deep dive into the psychology, mechanics, and ethics of making everything feel like a game.