Frontend Development

Next.js App Router — Modern Patterns for 2026

By Domain India Team · DomainIndia EngineeringPublished 10 min read
Knowledge base article
Contents (13 sections)
Key takeaways

Next.js App Router (stable since 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 where to deploy: a static export on shared hosting, or the Domain India App Platform or a VPS when you need server rendering.

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 React Suspense
FormsonSubmit handlerServer Actions (no fetch needed)
Metadatanext/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 ? 'Liked' : 'Like'}
    </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() {
  // Cached until revalidated (since Next.js 15, fetch is not cached unless you ask)
  const staticData = await fetch('https://api.example.com/static', {
    cache: 'force-cache',
  }).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: Promise<{ id: string }> }) {
  const { id } = await params; // params is a Promise since Next.js 15
  return (
    <form action={addComment.bind(null, 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

Combines static + dynamic in one route. The static shell is prerendered; dynamic parts inside Suspense boundaries stream in per request. The switch has moved between releases: Next.js 15 used an experimental ppr option and a per-route experimental_ppr flag, while Next.js 16 turns this behaviour on with the cacheComponents option. Check the documentation for the version you run.

ts
// next.config.ts (Next.js 16)
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  cacheComponents: true,
};

export default nextConfig;
tsx
// app/product/[id]/page.tsx
import { Suspense } from 'react';

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

Best of SSG + SSR. Partial prerendering needs a running Next.js server, so it does not apply to a static export.

Middleware — request-time logic

middleware.ts at project root — runs before every matched request. Next.js 16 renames this file to proxy.ts (and the exported function to proxy); the logic is the same.

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).*)'],
};

Use it for redirects, auth checks, A/B tests and geolocation routing. Keep it light: it runs on every matched request, and it needs a running Next.js server (it is not available in a static export). Treat it as a first gate only, and check the session again where the data is read.

Metadata API (SEO)

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

export async function generateMetadata(
  { params }: { params: Promise<{ slug: string }> }
): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPost(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 the page's 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): cacheComponents in next.config on Next.js 16

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 Domain India

Where a Next.js app can run depends on whether it needs a server at request time.

What you deployWhere it fitsNotes
Static export (output: 'export')cPanel, DirectAdmin or Webuzo shared hostingUpload the out/ folder; no SSR, Server Actions, middleware or ISR
Server-rendered app (SSR, ISR, Server Actions, PPR)App Platform or VPSNeeds a long-running Node.js process
App plus its own database and background jobsVPSFull root access; you run and patch the server yourself

Shared hosting: static export only. The old next export command was removed in Next.js 14. Set the output mode in your config instead, then run a normal build:

ts
// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  output: 'export',
  trailingSlash: true,            // /about/ -> out/about/index.html, simplest on Apache
  images: { unoptimized: true },  // the default image optimiser needs a server
};

export default nextConfig;
bash
npm run build     # next build writes the static site to out/

Build on your own computer or in CI, then upload the contents of out/ to public_html with the File Manager or FTP. A static export can still fetch data in the browser from an API hosted elsewhere. Anything that needs the server at request time (SSR, Server Actions, middleware, ISR, dynamic route handlers, partial prerendering) will not work in an export; the build tells you which routes block it.

We don't recommend running a server-rendered Next.js app on shared hosting. For SSR, use the App Platform or a VPS.

App Platform: Node.js apps are detected and built automatically, so a standard Next.js project with build and start scripts deploys without a Dockerfile. Deploy with Deploy Now or a deploy token in CI; pushing to Git does not deploy on its own. WebSockets are not supported. See Getting started with the App Platform.

VPS (full control): use the standalone output so the server only needs the files it runs:

bash
# next.config.ts: output: 'standalone'
npm ci
npm run build
cp -r public .next/standalone/ && cp -r .next/static .next/standalone/.next/
node .next/standalone/server.js    # listens on port 3000; set PORT to change it

Run it under systemd or PM2 behind nginx as a reverse proxy. See running Go applications on a VPS with systemd and a reverse proxy for the same pattern, and deploying Next.js: shared vs VPS for a fuller comparison. A Domain India VPS is self-managed.

Vercel: the simplest option and made by the Next.js team; costs grow with usage and some features are tied to its platform.

Cloudflare Workers: runs Next.js through the OpenNext adapter (@opennextjs/cloudflare), which replaced the older @cloudflare/next-on-pages. Good for global distribution.

Common pitfalls

Using useState in a Server Component
It throws an error. Add 'use client' or move the stateful part into a child component.
Fetching in loops
The N+1 problem. Batch the query or use Promise.all.
Forgetting 'use client' on interactive components
Runtime error that hooks cannot be used on the server.
Cache too aggressive
Users see stale content. Use revalidate or cache: 'no-store'.
Server Actions without revalidation
The page doesn't refresh after a mutation. Call revalidatePath or revalidateTag.
Bundle too large
Ship client components only when needed, and check the npm run build output.

FAQ

Pages Router or App Router for new projects?

App Router. Pages Router still works and is maintained, but new Next.js features are built for the App Router.

Next.js vs Remix vs Astro?

Next.js has the most features and the biggest ecosystem. Remix (now React Router) offers similar full-stack features with a different philosophy. Astro is content-focused, for blogs and docs. Pick Next.js for apps and Astro for content-heavy sites.

Can I host Next.js on shared hosting?

Only as a static export. Set output: 'export' in next.config, run next build and upload the out folder to public_html. The next export command was removed in Next.js 14. For server rendering, Server Actions or middleware, use the Domain India App Platform or a VPS.

Do Server Components replace APIs?

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

Server Actions vs traditional API routes?

Use Server Actions for forms and mutations triggered by the user, and route handlers for webhooks, external consumers and GraphQL.

Ready to deploy? Put a static export on cPanel hosting, run a server-rendered app on the App Platform, or take full control on a VPS. Not sure which fits? Open a support ticket.

Run your Next.js app with server rendering

Node.js apps are detected and built automatically, so a server-rendered Next.js app deploys without a Dockerfile.

See App Platform plans

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