Next.js 15 App Router patterns I use on every project
After shipping 20+ App Router apps, here are the patterns that keep code clean — server components by default, server actions for forms, and the layout tricks I lean on.
I have shipped 20+ App Router apps since Next.js 13.4. The patterns below are the ones I do NOT think about anymore — they are just default. If you are writing your first or second App Router app, copy them.
1. Server components by default. use client is a tax.
Every component is a server component until proven otherwise. The proof is one of:
- Needs
useState/useReducer - Needs
useEffect/useReffor browser APIs - Needs an event handler (
onClick,onChange) - Renders something that needs a browser API (canvas, IntersectionObserver)
Anything else is server. This rule alone shaves 30-50% off most JS bundles.
2. Co-locate data fetching with the component that uses it
// app/dashboard/page.tsx
export default async function DashboardPage() {
const user = await getUser();
return (
<div>
<UserHeader user={user} />
<RecentOrders /> {/* fetches its own data */}
<RevenueChart /> {/* fetches its own data */}
</div>
);
}
Each child component does its own fetch. Loading and error states are local. The page is a layout, not a data orchestrator.
3. Server Actions for every form
// app/contact/actions.ts
"use server";
import { z } from "zod";
const schema = z.object({
email: z.string().email(),
message: z.string().min(10),
});
export async function submitContact(formData: FormData) {
const parsed = schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) return { error: "Invalid input" };
await sendEmail(parsed.data);
return { success: true };
}
Then in the form:
<form action={submitContact}>
<input name="email" />
<textarea name="message" />
<button>Send</button>
</form>
No API route. No client fetcher. No JSON. The form works without JavaScript.
4. loading.tsx and error.tsx everywhere
These two files give you Suspense + Error Boundary for free at every route. Use them. Even a one-line loading.tsx is better than nothing — it stops the layout from blocking.
5. generateStaticParams for all known routes
// app/services/[category]/[slug]/page.tsx
export function generateStaticParams() {
return getAllServicePaths(); // returns [{ category, slug }, ...]
}
Every product page, service page, blog post, country page — pre-rendered at build. Vercel serves them from the edge cache. Sub-100ms TTFB.
6. ISR for content that changes
export const revalidate = 3600; // 1 hour
Or, even better, on-demand revalidation triggered from your CMS:
// app/api/revalidate/route.ts
import { revalidatePath } from "next/cache";
export async function POST(req: Request) {
const { secret, path } = await req.json();
if (secret !== process.env.REVAL_SECRET) return new Response("Nope", { status: 401 });
revalidatePath(path);
return Response.json({ revalidated: true });
}
7. not-found.tsx per route group
The default Next.js 404 is fine. But /services/not-found.tsx lets you say "we couldn't find that service — here are all our services" with relevant links. Higher conversion. Lower bounce.
8. Metadata via generateMetadata
For dynamic routes:
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.description,
alternates: { canonical: `/blog/${params.slug}` },
openGraph: { images: [post.cover] },
};
}
Single source of truth for SEO. No <Head> boilerplate.
9. Route groups for layout sharing
(marketing), (app), (admin) route groups let you have totally different layouts (different navs, footers, even fonts) without any URL impact.
10. cn() and tailwind-merge together
Tiny but daily-used:
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Lets you override Tailwind classes from a parent without specificity wars.
Patterns I avoid in 2026
- Pages Router (only when migrating an old codebase, never new).
- Class components (zero reason in 2026).
- Redux for new projects (zustand or server state).
getServerSideProps(use RSC + cache).<Link prefetch={false}>everywhere because someone got nervous (the prefetcher is good).
The mental model that helps most
Think of your App Router app as a server-rendered website that upgrades to interactive on islands you mark with use client. Not a SPA. Not a static site. A modern, server-first website with interactive bits where they are needed.
Once that clicks, every other pattern follows naturally.
If you are starting an App Router app and want a 30-minute architecture call, WhatsApp me. I have made enough of these decisions that the call usually saves a week of back-and-forth.
Author
Usama
I have spent the last 6+ years shipping production websites and apps from Pakistan for clients across 30+ countries. I work daily on Fiverr and Upwork, and partner directly with founders, agencies and local businesses on long-term builds.
More about Usama