Client Area

Next.js App Router — Modern Patterns for 2026

ByDomain India Team·DomainIndia Engineering
6 min readPublished 23 Apr 2026Updated 23 Jun 20262,215 views

In this article

  • 1App Router vs Pages Router — why change
  • 2Project layout
  • 3Server Components by default
  • 4Data fetching
  • 5Server Actions — mutations without fetch

Next.js App Router — Modern Patterns for 2026

TL;DR
Next.js App Router (introduced 2023) has replaced Pages Router as the default for new Next projects. This guide covers the patterns that matter for production — Server Components, Server Actions, streaming, partial prerendering, middleware, and deploying on DomainIndia VPS vs Vercel vs Cloudflare.

App Router vs Pages Router — why change

The old pages/ directory approach was fine but bolted routing atop client-side React. App Router makes the server first-class:

FeaturePages RouterApp Router
Server/client boundarygetServerSideProps / getStaticProps at page levelPer-component via 'use client'
LayoutsManually composedNative layout files
Data fetchingHook-based mostlyfetch() in Server Components
StreamingLimitedBuilt-in via <Suspense>
FormsonSubmit handlerServer Actions (no fetch needed)
Metadata<Head> componentmetadata export + generateMetadata

For new projects in 2026: always App Router. Pages Router receives maintenance but no new features.

Project layout

app/
├── layout.tsx               # root layout (wraps everything)
├── page.tsx                 # home page
├── loading.tsx              # shown while page is loading
├── error.tsx                # shown on errors
├── not-found.tsx            # 404
├── (marketing)/             # route group — doesn't add to URL
│   ├── about/page.tsx
│   └── pricing/page.tsx
├── (app)/                   # route group for authenticated routes
│   ├── layout.tsx           # different layout (e.g. with sidebar)
│   ├── dashboard/page.tsx
│   └── settings/page.tsx
├── api/                     # route handlers (former API routes)
│   └── users/route.ts
├── blog/
│   ├── page.tsx             # /blog
│   └── [slug]/page.tsx      # /blog/<slug>

Parentheses (group) don't affect URL. Brackets [slug] = dynamic segment.

Server Components by default

Every component is server-rendered unless marked 'use client'.

Server Component (default):

tsx
// app/blog/page.tsx
import { db } from '@/lib/db';

export default async function BlogList() {
  const posts = await db.post.findMany();
  return (
    <ul>
      {posts.map(p => <li key={p.id}>{p.title}</li>)}
    </ul>
  );
}
  • Runs on server
  • Can directly query DB, use secrets
  • Ships zero JS to client for this component
  • Can't use useState, useEffect, browser APIs

Client Component:

tsx
// app/components/LikeButton.tsx
'use client';
import { useState } from 'react';

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '❤️' : '🤍'}
    </button>
  );
}
  • Runs on server (for SSR) AND client (for interactivity)
  • Needs 'use client' directive at top
  • Use for anything interactive

Rule: ship as much as Server Components; use Client Components only when needed.

Data fetching

Built-in fetch in Server Components with caching:

tsx
export default async function Page() {
  // Static — cached indefinitely
  const staticData = await fetch('https://api.example.com/static').then(r => r.json());

  // Revalidate every 60s
  const dynamicData = await fetch('https://api.example.com/news', {
    next: { revalidate: 60 },
  }).then(r => r.json());

  // Always fresh
  const liveData = await fetch('https://api.example.com/stock', {
    cache: 'no-store',
  }).then(r => r.json());

  return <article>...</article>;
}

Next.js dedupes concurrent identical fetches per render. No extra query.

Server Actions — mutations without fetch

tsx
// app/post/[id]/page.tsx
import { revalidatePath } from 'next/cache';

async function addComment(postId: string, formData: FormData) {
  'use server';
  const text = formData.get('text') as string;
  await db.comment.create({ data: { postId, text } });
  revalidatePath(`/post/${postId}`);
}

export default async function Post({ params }: { params: { id: string } }) {
  return (
    <form action={addComment.bind(null, params.id)}>
      <textarea name="text" />
      <button type="submit">Comment</button>
    </form>
  );
}
  • Function executes on server
  • No fetch/API route needed
  • revalidatePath refreshes cached data
  • Progressive enhancement — works without JavaScript

Streaming with Suspense

Long-loading components don't block the page:

tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <Header />
      <Suspense fallback={<Skeleton />}>
        <SlowReport />
      </Suspense>
      <Suspense fallback={<Skeleton />}>
        <EvenSlowerReport />
      </Suspense>
    </div>
  );
}

Header shows instantly; reports stream in when ready. Shell is fast; content fills in.

Partial Prerendering (2026 stable)

Combines static + dynamic in one route. Static parts ship from CDN; dynamic parts stream from origin.

tsx
// app/product/[id]/page.tsx
export const experimental_ppr = true;

import { Suspense } from 'react';

export default function Product({ params }) {
  return (
    <div>
      {/* Static — prerendered */}
      <ProductDescription id={params.id} />
      <Suspense fallback={<div>Loading cart...</div>}>
        {/* Dynamic — user-specific, rendered per request */}
        <UserCart />
      </Suspense>
    </div>
  );
}

Best of SSG + SSR.

Middleware — edge logic

middleware.ts at project root — runs before every matched request:

typescript
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Redirect non-www to www
  if (!request.nextUrl.host.startsWith('www.')) {
    return NextResponse.redirect(new URL(`https://www.${request.nextUrl.host}${request.nextUrl.pathname}`));
  }

  // Auth gate
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    const token = request.cookies.get('session');
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  // A/B test
  const bucket = request.cookies.get('ab')?.value || (Math.random() > 0.5 ? 'a' : 'b');
  const response = NextResponse.next();
  response.cookies.set('ab', bucket);
  return response;
}

export const config = {
  matcher: ['/((?!api|_next|favicon.ico).*)'],
};

Runs at the edge. Use for redirects, auth, A/B tests, geolocation routing.

Metadata API (SEO)

tsx
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';

export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: `${post.title} | Your Blog`,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
    twitter: { card: 'summary_large_image' },
  };
}

Next.js generates <head> tags automatically.

Static vs ISR vs SSR vs PPR

tsx
// Static (at build time) — default behaviour
export default async function Page() {
  const data = await fetch('...'); return <div>{data}</div>;
}

// ISR (Incremental Static Regeneration)
export const revalidate = 60; // seconds

// SSR (every request fresh)
export const dynamic = 'force-dynamic';

// PPR (partial, as above)
export const experimental_ppr = true;

Pick based on data freshness:

Data freshnessStrategy
Never changes (docs, landing page)Static
Changes hourlyISR with revalidate: 3600
Per-user (dashboard)SSR
Mixed (user header + static product)PPR

Deployment on DomainIndia

VPS (recommended for self-hosting):

bash
npm run build           # produces .next/ build output
npm install --production
node_modules/.bin/next start -p 3000

systemd + nginx reverse proxy. See our Go/Rust deployment articles for pattern.

Shared cPanel:

Use next build && next export for static export (no dynamic features). Upload out/ to public_html.

For dynamic Next.js: needs Node.js app support on cPanel. Works for small apps.

Vercel: simplest but vendor-locked and $$ at scale.

Cloudflare Workers: runs Next.js via @cloudflare/next-on-pages. Good for global distribution.

Common pitfalls

FAQ

Q Pages Router or App Router for new projects?

App Router, unquestionably. Pages is legacy.

Q Next.js vs Remix vs Astro?

Next.js — most features, biggest ecosystem, enterprise-ready. Remix — similar features, smaller ecosystem, different philosophy. Astro — content-focused (blogs, docs), per-component framework choice. Pick Next for apps, Astro for content-heavy sites.

Q Can I host Next.js on shared hosting?

Static export yes. Dynamic rendering — needs Node.js app support. VPS is simpler for serious use.

Q Do Server Components replace APIs?

For internal data fetching — often yes. For mobile apps or third-party integrations, you still need REST/GraphQL APIs.

Q Server Actions vs traditional API routes?

Actions for forms + mutations triggered by user. API routes for webhooks, external consumers, GraphQL.

Deploy Next.js on a DomainIndia VPS — no vendor lock-in. Start with VPS

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket