Technical
Next.js App Router Patterns I Actually Use
The App Router is Next.js's default, but most tutorials still teach the old Pages pattern. The result is a generation of developers using App Router like it is Pages with extra steps. Here are the patterns I actually use daily for production Next.js apps.
Server Components by Default
Every component is a Server Component unless I mark it otherwise. Server Components do not ship JavaScript to the client. They render on the server and send HTML. For most of my pages, that is exactly what I want.
// app/posts/page.tsx - no 'use client'
async function PostsPage() {
const posts = await fetchPosts();
return <PostList posts={posts} />;
}Notice the async. Server Components can be async. That alone removes 90% of the useEffect data-fetching boilerplate from Pages Router days.
Client Components Only at the Leaves
I push 'use client' as far down the tree as possible. A page might be a Server Component that renders a Client Component only for the interactive search bar. The rest stays on the server.
Layouts for Shared UI
layout.tsx at any level wraps all child pages. Put the nav here, not in every page:
// app/(dashboard)/layout.tsx
export default function DashboardLayout({ children }) {
return (
<>
<DashboardNav />
<main>{children}</main>
</>
);
}Route groups (the parentheses) let me organize without adding URL segments. /dashboard/posts and /dashboard/settings both live inside (dashboard) but the URL stays clean.
Data Fetching at the Component Level
Any Server Component can fetch data. I do not pass data down through props. Each component fetches what it needs. React deduplicates identical fetches in the same render pass.
async function PostTitle({ id }) {
const post = await fetchPost(id);
return <h1>{post.title}</h1>;
}Loading and Error Boundaries
loading.tsx next to a page automatically shows while the page's Server Component awaits data. error.tsx catches errors at the route level. I no longer write manual isLoading state for most pages.
Server Actions for Mutations
Forms use Server Actions. No API route needed for simple mutations:
async function createPost(formData: FormData) {
'use server';
await db.posts.create({ title: formData.get('title') });
}That function runs on the server when the form submits. The client never sees the database code. It is one of the cleanest patterns Next.js has shipped.
See the Next.js App Router documentation for the official conventions and the patterns I riff on here.
RELATED READING
The Consulting Shift I Am Making In Year Two
After a year of writing and building, my consulting practice is changing shape. Shorter engagements. Sharper outcomes.
ReadThe Frontend Shift: Shipping Less JavaScript In Year Two
A year ago I reached for Next.js for everything. This year I often reach for nothing.
ReadThe Serverless Lesson I Would Write On A Sticky Note
After a year of shipping serverless projects, one rule explains most of the wins and all of the losses.
Read