HomeBlogFrontend
Frontend

Next.js App Router: Patterns That Actually Scale

Server Components, streaming, parallel routes, and intercepting routes — the App Router features that transform Next.js from a framework into a platform.

Nov 18, 2024 11 min read 31.5k views
Next.js React Server Components App Router

The App Router isn't just a new file structure — it's a fundamentally different model for building web apps. Server Components, streaming, and the new data fetching patterns change how you think about where logic lives. After migrating three large production apps, here are the patterns that actually work.

Server Components: The Right Mental Model

The key insight is that Server Components are not fetched components — they run on the server at request time and their output is serialized HTML. They never re-render, never have state, and their code never ships to the client. This is a fundamentally different primitive from useEffect + fetch.

tsx
// This runs on the server — db query never exposed to client
async function UserDashboard({ userId }) {
  // Direct DB access — no API route needed
  const user = await db.users.findUnique({ where: { id: userId } });
  const orders = await db.orders.findMany({ where: { userId } });

  return (
    <div>
      <UserProfile user={user} />
      <OrderList orders={orders} />
    </div>
  );
}
💡 Tip

Move the Client/Server boundary as far down the tree as possible. A component only needs 'use client' if it uses browser APIs, event handlers, or React hooks. Everything else can stay on the server.

Parallel Routes for Complex Layouts

Parallel routes let you render multiple pages in the same layout simultaneously — each with its own loading and error states. This is the App Router's answer to dashboard layouts where the sidebar, main content, and modals load independently.

Streaming with Suspense Boundaries

Wrap slow data fetches in Suspense to stream the fast parts of your page immediately, then progressively fill in slower sections. Users see content within 100ms instead of waiting 2 seconds for the slowest query to finish.

tsx
export default function Dashboard() {
  return (
    <>
      <StaticHeader />  {/* Renders instantly */}
      <Suspense fallback={<AnalyticsSkeleton />}>
        <AnalyticsPanel />  {/* Streams in when ready */}
      </Suspense>
      <Suspense fallback={<FeedSkeleton />}>
        <ActivityFeed />   {/* Streams in independently */}
      </Suspense>
    </>
  );
}

Found this useful?

Share it with your network