21. Advanced Next.js
Overview
The Next.js App Router introduces a file‑system based routing model that goes beyond simple page mapping. By leveraging parallel routes, intercepting routes, and route groups, developers can compose complex layouts without adding extra URL segments. Coupled with advanced caching strategies, Edge Runtime capabilities, and multi‑tenant patterns, these features enable the creation of highly modular, fast, and scalable applications.
Parallel Routes
Parallel routes allow multiple page components to be rendered simultaneously within a single layout. This is ideal for dashboards where independent sections—such as a sidebar, a feed, and a metrics panel—should appear together without nesting routes.
Folder Structure
app/
└─ dashboard/
├─ layout.tsx // shared dashboard layout
├─ page.tsx // optional default view
├─ @sidebar/
│ └─ page.tsx // renders in the sidebar slot
├─ @feed/
│ └─ page.tsx // renders in the feed slot
└─ @metrics/
└─ page.tsx // renders in the metrics slot
When a user navigates to /dashboard, Next.js automatically renders layout.tsx and fills the @sidebar, @feed, and @metrics slots with their respective page.tsx components. No additional routes are needed, and each slot can be navigated independently via link components that target the specific slot (e.g., <Link href="/dashboard/@feed">).
Usage Example
In
app/dashboard/layout.tsxdefine named slots:export default function DashboardLayout({ children, sidebar, feed, metrics }) { return ( <div className="flex h-[100vh]"> <aside className="w-64">{sidebar}</aside> <main className="flex-1 p-4">{feed}</main> <aside className="w-64">{metrics}</aside> </div> ); }
Intercepting Routes
Intercepting routes let a segment “catch” a navigation and render its content while preserving the original URL in the browser’s address bar. This pattern is commonly used for modals, drawers, or overlays that should not alter the perceived navigation.
Convention
To intercept a route, wrap the segment in parentheses and prepend two dots to indicate a relative lookup from the current segment:
app/
└─ gallery/
├─ layout.tsx
├─ page.tsx // /gallery
└─ (..)photo/
└─ [id]/
└─ page.tsx // intercepts /gallery/[id]
When a user clicks a photo link pointing to /gallery/123, Next.js renders app/gallery/(..)photo/[id]/page.tsx (often a modal) while the URL stays as /gallery/123. If the user refreshes or navigates directly to that URL, the intercepted component still appears, providing a seamless experience.
Implementation Tips
- Place the intercepting file inside a folder that mirrors the intercepted path, prefixed with
(..). - Use
useRouter()fromnext/navigationto readparams.idfor data fetching. - Optionally add a
loading.tsxexport const PhotoModal = () => { /* modal UI */ }; } to keep the modal isolated from the underlying gallery layout.
Route Groups
Route groups let you organize files inside the app directory without affecting the URL path. This is useful for separating concerns by team, feature, or intent while avoiding route collisions in large codebases.
Syntax
Wrap a folder name in parentheses: (marketing), (app), (auth). The contents of the group are routed as if they were placed directly under app.
Example
app/
├─ (marketing)/
│ ├─ about.tsx // → /about
│ └─ terms.tsx // → /terms
├─ (app)/
│ ├─ dashboard.tsx // → /dashboard
│ └─ settings.tsx // → /settings
└─ (auth)/
├─ sign-in.tsx .tsx // → /sign-in
└─ sign-up.tsx // → /sign-up
All four routes resolve to the top‑level paths (/about, /terms, /dashboard, /settings, /sign-in, /sign-up) despite being nested in distinct groups. This enables teams to own their folders without worrying about accidental overlapping routes.
Advanced Caching
Next.js provides granular control over caching through cache tags and revalidation APIs, allowing developers to invalidate only the data that actually changed while keeping other content fast.
Cache Tags
Tags are strings attached to fetched data. When the underlying data mutates, calling revalidateTag purges the cache for all entries sharing that tag.
Example: Product Catalog
// app/products/[id]/page.tsx
export const revalidate = 0; // disable static generation for demo
export default async function ProductPage({ params }) {
const product = await fetch(`https://api.example.com/products/${params.id}`, {
next: { tags: [`product-${params.id}`] },
}).then(res => res.json());
return (
<div>
<h1>{product.name}</h1>
<p>{product.description}</p>
</div>
);
}
// After updating product 123:
await revalidateTag('product-123'); // fetches fresh data on next request
Multiple tags can be combined: next: { tags: ['product-123', 'collection-featured'] }. This enables fine‑grained purging—for instance, updating a collection revalidates all products tagged with that collection without affecting unrelated blog posts.
Revalidation APIs
Next.js offers both time‑based and on‑demand revalidation.
- Time‑based (ISR): Set
export const revalidate = 60;to get a stale‑while‑revalidate window of 60 seconds. - Stale‑if‑error: Use
export const unstable_revalidate = 60;andexport const unstable_revalidateOnError = true;to keep serving stale content if the revalidation fails. - On‑demand path/tag: Call
revalidatePath('/blog')orrevalidateTag('collection')from an API route, webhook, or server action.
Example: Blog Index with ISR
// app/blog/page.tsx
export const revalidate = 60; // revalidate at most once per minute
export default async function Blog() {
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'] },
}).then(res => res.json());
return (
<ul>
{posts.map(p => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
Edge Runtime
Deploying code to the Edge brings computation closer to users, reducing latency dramatically. Next.js allows both API routes and middleware to run on the Edge (Vercel Edge Functions or Cloudflare Workers).
Edge Functions
An Edge Function is an API route or route.ts file that opts into the Edge runtime via export const config = { runtime: 'edge' };. It executes at the nearest Cloudflare or Vercel node.
Geolocation Middleware Example
// app/api/geo/route.ts
export const config = { matcher: '/api/geo', runtime: 'edge' };
export default function handler(req: Request) {
const country = req.geo?.country ?? 'US';
return new Response(JSON.stringify({ welcome: `Hello from ${country}!` }), {
headers: { 'content-type': 'application/json' },
});
}
This runs in ~30 ms for a user in Europe versus ~120 ms from a central US region.
Edge Middleware
Middleware runs before caching decisions, enabling redirects, header modifications, or A/B testing based on cookies, headers, or geolocation.
Consent Cookie Middleware
// middleware.ts
export const config = { matcher: ['/privacy', '/terms'] };
export default function middleware(req) {
const consent = req.cookies.get('consent');
if (!consent) {
const consentUrl = new URL('/consent', req.url);
consentUrl.searchParams.set('redirect', req.url);
return Response.redirect(consentUrl);
}
// Optionally add security headers
const res.headers.set('Content-Security-Policy', "default-src 'self'");
res.headers.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
return res;
}
Because middleware executes on the Edge, the redirect decision is made before any HTML is generated, preserving performance.
Multi‑Tenant Architecture
Next.js makes it straightforward to build SaaS‑style applications where each tenant (customer, organization) gets isolated data and possibly customized styling while sharing core infrastructure.
Tenant Routing (Subdomain or Path‑Based)
Dynamic segments can capture the tenant identifier from either the subdomain or the URL path.
Path‑Based Example
app/
└─ [tenant]/
├─ layout.tsx // reads params.tenant
├─ page.tsx // tenant‑specific home
└─ dashboard/
└─ page.tsx // tenant dashboard
Accessing https://myapp.com/acme/dashboard sets params.tenant = 'acme'. The layout can then load a tenant‑specific CSS file:
// app/[tenant]/layout.tsx
export default function TenantLayout({ children, params }) {
const tenant = params.tenant;
return (
<>
<link rel="stylesheet" href="/tenants/${tenant}.css" />
<main>{children}</main>
</>
);
}
Subdomain‑Based Example (using Vercel’s domain middleware or a custom server)
Configure a catch‑all route app/[tenant]/… and rely on the hosting platform to forward acme.myapp.com to myapp.com/acme (or use headers() to read host). The same params.tenant value is then available.
Tenant Databases
Data isolation can be achieved via a schema‑per‑tenant approach or a shared database with a tenant_id column. Below is a Prisma example using the shared‑table method.
Prisma Query with Tenant Filter
// app/[tenant]/dashboard/page.tsx
export default async function TenantDashboard({ params }) {
const posts = await prisma.post.findMany({
where: { tenantId: params.tenant }, // ensures isolation
include: { author: true },
});
return (
<section>
{posts.map(p => (
<article key={p.id}>
<h2>{p.title}</h2>
<p>by {p.author.name}</p>
</article>
))}
</section>
);
}
When using a schema‑per‑tenant setup, you would switch the Prisma client’s datasource at runtime based on params.tenant, but the querying pattern remains the same.
Putting It All Together: A Dashboard Example
Imagine a SaaS product where each tenant sees a customizable dashboard composed of parallel slots (sidebar, feed, metrics). The dashboard uses intercepting routes to open a full‑size chart in a modal without leaving the dashboard URL, leverages Edge Middleware to enforce tenant‑specific subdomain redirects, and employs cache tags to refresh widget data instantly when the underlying dataset changes.
Folder Sketch
app/
├─ (app)/
│ └─ [tenant]/
│ ├─ layout.tsx // loads tenant CSS, checks auth
│ ├─ page.tsx // tenant home
│ ├─ dashboard/
│ │ ├─ layout.tsx // defines @sidebar, @feed, @metrics slots
│ │ ├─ page.tsx // optional default view
│ │ ├─ @sidebar/
│ │ │ └─ page.tsx // navigation, tenant logo
│ │ ├─ @feed/
│ │ │ └─ page.tsx // list of recent activities (ISR, tag: feed-${tenant})
│ │ └─ @metrics/
│ │ └─ page.tsx // charts (intercepting route for detail)
│ │ ├─ page.tsx// small preview chart
│ │ └─ (..)chart/
│ │ └─ [id]/
│ │ └─ page.tsx // full‑size modal chart
│ └─ api/
│ └─ revalidate/
│ └─ route.ts // webhook handler: await revalidateTag('feed-${tenant}')
└─ middleware.ts // Edge middleware: subdomain → tenant path
When a user visits https://acme.myapp.com, the middleware rewrites the request to /acme, the layout loads acme.css, and the dashboard renders the three slots in parallel. Clicking a chart preview triggers the intercepting route (..)chart/[id]/page.tsx, showing a detailed modal while the URL stays /acme/dashboard. A webhook from the backend calls revalidateTag('feed-acme'), causing the feed slot to refetch fresh data on the next request.
Summary
By mastering parallel and intercepting routes, route groups, cache tags and revalidation APIs, Edge Functions and middleware, and multi‑tenant routing with data isolation, you can build Next.js applications that are:
- **Modular** – UI pieces are independently versioned and placed via slots.
- **Fast** – Edge execution and fine‑grained caching keep latency low and data fresh.
- **Scalable** – Tenant‑aware architecture isolates data and styling while sharing core logic.
- **Maintainable** – Clear file‑system conventions reduce routing collisions and simplify onboarding.
These patterns, when combined, empower developers to deliver enterprise‑grade SaaS platforms, content‑rich portals, and high‑performance dashboards—all within the Next.js ecosystem.