| name | nextjs-patterns |
| description | Next.js best practices, server components, app router patterns, caching strategies, and full-stack architecture |
| metadata | {"author":"cosmicstack-labs","version":"1.0.0","category":"frontend","tags":["nextjs","react","server-components","app-router","full-stack","ssr","ssg","isr"]} |
Next.js Patterns
Comprehensive guide to building production-grade Next.js applications with the App Router, Server Components, and modern React patterns.
Core Architecture
App Router vs Pages Router
| Aspect | App Router | Pages Router |
|---|
| Components | Server Components by default | Client Components only |
| Routing | File-system based with layout nesting | File-system based, flat |
| Data Fetching | Server-side fetch, use, cache primitives | getServerSideProps, getStaticProps |
| Loading States | loading.js files | Manual implementation |
| Error Handling | error.js files | Manual implementation |
| Streaming | Built-in with Suspense boundaries | Not supported |
Project Structure
my-app/
├── app/
│ ├── layout.tsx # Root layout
│ ├── page.tsx # Home page
│ ├── loading.tsx # Root loading state
│ ├── error.tsx # Root error boundary
│ ├── not-found.tsx # 404 page
│ ├── (auth)/ # Route group
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── dashboard/
│ │ ├── layout.tsx # Dashboard layout
│ │ ├── page.tsx # Dashboard home
│ │ ├── loading.tsx
│ │ └── settings/
│ │ └── page.tsx
│ └── api/
│ └── [...route]/route.ts # API handlers
├── components/
│ ├── ui/ # Shared UI components
│ └── features/ # Feature-specific components
├── lib/
│ ├── db.ts # Database client
│ ├── auth.ts # Auth utilities
│ └── utils.ts # Shared utilities
└── public/
└── images/
Server Components (RSC)
When to Use Server vs Client
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
return <ProductDetail product={product} />;
}
'use client';
function AddToCartButton({ productId }: { productId: string }) {
const [added, setAdded] = useState(false);
return (
<button onClick={() => { addToCart(productId); setAdded(true); }}>
{added ? 'Added!' : 'Add to Cart'}
</button>
);
}
Composing Server and Client Components
async function ProductPage({ id }: { id: string }) {
const product = await getProduct(id);
return (
<div>
<ProductDetails product={product} />
<AddToCartButton productId={id} />
</div>
);
}
'use client';
import ServerComponent from './ServerComponent';
function ClientPage() {
return <ServerComponent />;
}
'use client';
function ClientLayout({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children} {/* Server component rendered here */}
</div>
);
}
Data Fetching Patterns
Server-Side Data Fetching
async function ProductsPage() {
const products = await db.product.findMany({
take: 20,
orderBy: { createdAt: 'desc' },
});
return <ProductGrid products={products} />;
}
async function DashboardPage() {
const [user, posts, analytics] = await Promise.all([
getCurrentUser(),
getRecentPosts(),
getAnalytics(),
]);
return (
<DashboardShell user={user}>
<PostList posts={posts} />
<AnalyticsChart data={analytics} />
</DashboardShell>
);
}
Revalidation Strategies
export const revalidate = 3600;
'use server';
export async function publishPost(formData: FormData) {
const post = await createPost(formData);
revalidateTag('posts');
revalidatePath('/blog');
redirect(`/blog/${post.slug}`);
}
export const dynamic = 'force-dynamic';
export const dynamic = 'force-static';
Streaming with Suspense
import { Suspense } from 'react';
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<DashboardSkeleton />}>
<DashboardContent />
</Suspense>
<Suspense fallback={<ActivitySkeleton />}>
<RecentActivity />
</Suspense>
</div>
);
}
async function DashboardContent() {
const data = await fetchDashboardData();
return <DashboardCharts data={data} />;
}
async function RecentActivity() {
const activity = await fetchRecentActivity();
return <ActivityFeed items={activity} />;
}
Route Handlers & API Patterns
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '20');
const products = await db.product.findMany({
skip: (page - 1) * limit,
take: limit,
});
return Response.json({ products, page, limit });
}
export async function POST(request: Request) {
const body = await request.json();
const product = await db.product.create({ data: body });
return Response.json(product, { status: 201 });
}
Common Patterns
Route Groups for Organization
Middleware for Auth
export function middleware(request: NextRequest) {
const token = request.cookies.get('session');
const { pathname } = request.nextUrl;
if (pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
Error Boundaries
'use client';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={() => reset()}>Try again</button>
</div>
);
}
Common Mistakes
- Overusing client components: Default to Server Components. Only add
'use client' when you need interactivity.
- Nesting fetch calls sequentially: Use
Promise.all() for independent data fetches.
- Ignoring caching defaults: Understand Next.js fetch caching. Use
no-store or revalidate explicitly.
- Mixing server/client component boundaries incorrectly: Pass Server Components as children to Client Components, don't import them.
- Forgetting error and loading states: Always provide error boundaries and loading skeletons for streaming.