Back to Blog
EngineeringJanuary 20, 202611 min read

Next.js App Router Patterns We Use in Production

Next.js App Router Patterns We Use in Production

After building several production applications on the Next.js App Router, some patterns have proven genuinely useful and others turned out to be over-engineered solutions to problems that do not exist. Here is what is actually worth adopting.

Server Components: The Default That Makes Sense

Components are server components by default. If a component does not use browser APIs, event handlers, or React state, it runs on the server. This changes how you think about data fetching:

// app/dashboard/page.tsx (server component, no "use client" needed)
export default async function DashboardPage() {
  const metrics = await db.query("SELECT * FROM daily_metrics WHERE ...");
  return (
    <div>
      <h1>Dashboard</h1>
      <MetricsGrid data={metrics} />
    </div>
  );
}

No useEffect. No loading state management. No API route to fetch through. Only add "use client" when you need interactivity (onClick, useState, useEffect) or browser-only APIs. Push it as far down the component tree as possible.

The Pattern That Works: Server Parent, Client Leaf

Keep pages and layouts as server components. Only make leaf components that need interactivity into client components. Pass server-fetched data down as props:

// app/products/page.tsx (server)
export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <div>
      <ProductFilters /> {/* client: has onClick handlers */}
      <ProductList products={products} /> {/* server: just renders data */}
    </div>
  );
}

Streaming with Suspense: Worth It for Slow Data

Suspense boundaries let you stream parts of the page as they become ready:

export default function DashboardPage() {
  return (
    <div>
      <QuickStats /> {/* fast, renders immediately */}
      <Suspense fallback={<ChartSkeleton />}>
        <RevenueChart /> {/* slow aggregation, streams in when ready */}
      </Suspense>
    </div>
  );
}

This is excellent for dashboards with mixed data sources. For simple CRUD pages where all queries take 50ms, wrapping everything in Suspense adds complexity without visible benefit. Do not add it preemptively.

Parallel Routes for Modals: Powerful but Complex

Parallel routes (@folder convention) let you render URL-backed modals. The user can share a link to the modal state, and the back button closes it. This is perfect for Instagram-style photo overlays.

For simple confirmation dialogs or settings panels, just use a client-side modal library. The parallel route approach has real edge cases around navigation state and default.tsx files that will cost you debugging time.

Route Groups: Underrated

Route groups (parenthesized folders) organize routes without affecting the URL:

app/
  (marketing)/
    layout.tsx       <- full-width nav
    page.tsx         <- /
    pricing/page.tsx <- /pricing
  (app)/
    layout.tsx       <- sidebar nav
    dashboard/page.tsx <- /dashboard
  (auth)/
    layout.tsx       <- minimal, no nav
    login/page.tsx   <- /login

Each group gets its own layout without nesting. Clean URLs. Zero mental overhead. One of the most practical App Router features.

Metadata API: SEO Done Right

Type-safe, colocated with the page, supports async data fetching:

export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { title: post.title, images: [post.coverImage] },
  };
}

Use it everywhere. It is a straightforward improvement over the old next/head approach.

Patterns I Would Skip

Intercepting routes for everything. The mental model of (.), (..), (...) path matching is confusing to onboard new developers onto. Use them only where URL-backed modals genuinely matter.

Overusing loading.tsx. If your queries are fast, the loading state flashes for 100ms and creates a worse experience than no loading state. Only add it to routes where fetching takes more than 300ms.

Server Actions for everything. Great for form submissions. Using them as a replacement for a proper API layer gets messy when external clients need the same endpoint.

What Actually Matters

The patterns that deliver real value: server components by default, route groups for layout organization, the metadata API, and selective Suspense for slow data. Everything else is situational. Start simple, add complexity only when you hit a real problem the advanced pattern solves.