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:
| Feature | Pages Router | App Router |
|---|---|---|
| Server/client boundary | getServerSideProps / getStaticProps at page level | Per-component via 'use client' |
| Layouts | Manually composed | Native layout files |
| Data fetching | Hook-based mostly | fetch() in Server Components |
| Streaming | Limited | Built-in via React Suspense |
| Forms | onSubmit handler | Server Actions (no fetch needed) |
| Metadata | next/head component | metadata 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):
// 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:
// 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:
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
// 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
revalidatePathrefreshes cached data- Progressive enhancement — works without JavaScript
Streaming with Suspense
Long-loading components don't block the page:
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.
// next.config.ts (Next.js 16)
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
cacheComponents: true,
};
export default nextConfig;// 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.
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)
// 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
// 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 16Pick based on data freshness:
| Data freshness | Strategy |
|---|---|
| Never changes (docs, landing page) | Static |
| Changes hourly | ISR 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 deploy | Where it fits | Notes |
|---|---|---|
Static export (output: 'export') | cPanel, DirectAdmin or Webuzo shared hosting | Upload the out/ folder; no SSR, Server Actions, middleware or ISR |
| Server-rendered app (SSR, ISR, Server Actions, PPR) | App Platform or VPS | Needs a long-running Node.js process |
| App plus its own database and background jobs | VPS | Full 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:
// 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;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:
# 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 itRun 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
useState in a Server Component'use client' or move the stateful part into a child component.Promise.all.'use client' on interactive componentsrevalidate or cache: 'no-store'.revalidatePath or revalidateTag.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.
Node.js apps are detected and built automatically, so a server-rendered Next.js app deploys without a Dockerfile.
See App Platform plans