Frontend Development

Next.js App Router — Modern Patterns for 2026

By Domain India Team · DomainIndia EngineeringPublished · Updated 7 min read
Knowledge base article
Contents (13 sections)
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

code
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

Using useState in Server Component
error. Add 'use client' or extract to child component.
Fetching in loops
N+1 problem. Batch or use Promise.all.
Forgetting 'use client' on interactive components
runtime error "cannot use hooks on server".
Cache too aggressive
users see stale content. Use revalidate or cache: 'no-store'.
Server Actions without revalidation
page doesn't refresh after mutation. Call revalidatePath or revalidateTag.
Bundle too large
ship client components only when needed. Check npm run build output.

FAQ

Pages Router or App Router for new projects?

App Router, unquestionably. Pages is legacy.

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.

Can I host Next.js on shared hosting?

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

Do Server Components replace APIs?

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

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 answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Next.js App Router — Modern Patterns (Server Components, PPR, Actions)