← All posts

Next.js at Scale: Patterns I Keep Coming Back To

App Router, React Server Components, ISR, and edge middleware. The patterns that survive contact with real users and real deadlines.

Firas El-Jerdy

Firas El-Jerdy

Innovation and Development Engineer

#nextjs#react#web2025-12-15 · 8 min
Share Post

I have been building with Next.js since the Pages Router era. The App Router was a rough transition. But after shipping several production apps with it, I have settled on patterns that I reach for every time.

The first pattern is aggressive server component usage. My default is server components everywhere. Client components are an opt-in exception, not the default. This sounds obvious when you read the docs, but in practice, most teams sprinkle 'use client' liberally because it is easier. The cost shows up later: larger bundles, slower initial loads, and hydration mismatches that are painful to debug.

I keep client components small and leaf-level. A toggle button. A search input with local state. A Framer Motion animation wrapper. The page layout, data fetching, and content rendering stay on the server. This means my JavaScript bundle only contains interactive behavior, not structural markup. On one project, this cut the client-side JS by 40%.

The second pattern is ISR with on-demand revalidation. Static generation is fast and cheap, but stale data is a UX problem. ISR gives you the best of both: static speed with dynamic freshness. I set a revalidation window of 60 seconds for most pages and add on-demand revalidation webhooks for content that needs to update immediately. The combination means users almost always hit the cache, but the cache is never more than a minute stale.

The third pattern is route-level code splitting with parallel routes. App Router lets you define loading states, error boundaries, and independent data fetching per route segment. I structure my layouts so that the shell loads instantly and each section streams in as its data resolves. The user sees a complete page frame immediately, with content filling in progressively. It feels fast even when the data is slow.

Edge middleware is the fourth pattern, and it is the one most teams underuse. I run authentication checks, A/B test bucketing, and geo-based redirects at the edge. This means the decision happens in the CDN before the request even hits your origin server. For auth-gated apps, the difference is dramatic - unauthorized users get redirected in under 50ms instead of waiting for a full server round trip.

Metadata and SEO deserve their own mention. The generateMetadata function in App Router is powerful but easy to get wrong. I create a shared metadata utility that takes a page title and description and returns a complete metadata object with Open Graph tags, Twitter cards, canonical URLs, and structured data. Every page imports this utility. Consistency is automatic.

The pattern I have stopped using is API routes for internal data fetching. If your React Server Component can fetch data directly during render, there is no reason to add an API route as an intermediary. API routes add latency, another deployment target, and another thing to maintain. I only use them for external webhook endpoints and third-party integrations.

One last thing: TypeScript end-to-end. Server components, client components, API routes, data fetching, all typed. If I change a database column, the type error propagates from the query to the component that renders it. This catches bugs at build time that would otherwise appear as undefined values in production. The investment in typing pays for itself within the first week.

Enjoyed this?

Share it or check out more posts.