v1.0.0 cosmicstack-labs
Next.js Patterns
Next.js best practices, server components, app router patterns, caching strategies, and full-stack architecture
View source0 downloads
nextjsreactserver-componentsapp-routerfull-stackssrssgisr
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#
// ✅ SERVER COMPONENT (default) — Use for:
// - Data fetching from databases/APIs
// - Accessing backend resources directly
// - Keeping sensitive logic on server
// - Reducing client bundle size
// - SEO-critical content
async function ProductPage({ params }: { params: { id: string } }) {
const product = await db.product.findUnique({ where: { id: params.id } });
return <ProductDetail product={product} />;
}
// ✅ CLIENT COMPONENT — Use for:
// - Interactivity (onClick, onChange, etc.)
// - useState, useReducer, useEffect
// - Browser-only APIs
// - Custom hooks
// - Event listeners
'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#
// ✅ CORRECT: Server Component wrapping Client Component
async function ProductPage({ id }: { id: string }) {
const product = await getProduct(id);
// Server component passes data as props to client component
return (
<div>
<ProductDetails product={product} />
<AddToCartButton productId={id} />
</div>
);
}
// ❌ WRONG: Client Component importing Server Component
'use client';
import ServerComponent from './ServerComponent'; // ❌ Won't work
function ClientPage() {
return <ServerComponent />; // Error: Cannot import server component into client
}
// ✅ CORRECT: Pass Server Component as children
'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#
// app/products/page.tsx
async function ProductsPage() {
// Direct database access — no API route needed
const products = await db.product.findMany({
take: 20,
orderBy: { createdAt: 'desc' },
});
return <ProductGrid products={products} />;
}
// With parallel fetching for multiple data sources
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#
// Time-based revalidation (ISR)
export const revalidate = 3600; // Revalidate every hour
// On-demand revalidation (revalidateTag / revalidatePath)
'use server';
export async function publishPost(formData: FormData) {
const post = await createPost(formData);
revalidateTag('posts');
revalidatePath('/blog');
redirect(`/blog/${post.slug}`);
}
// Dynamic data — no caching
export const dynamic = 'force-dynamic';
// Static data — cache forever
export const dynamic = 'force-static';Streaming with Suspense#
// app/dashboard/page.tsx
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(); // This suspends
return <DashboardCharts data={data} />;
}
async function RecentActivity() {
const activity = await fetchRecentActivity(); // This loads independently
return <ActivityFeed items={activity} />;
}Route Handlers & API Patterns#
// app/api/products/route.ts
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#
// app/(marketing)/page.tsx — Public marketing pages
// app/(dashboard)/page.tsx — Authenticated dashboard
// app/(auth)/login/page.tsx — Auth pages with different layoutMiddleware for Auth#
// middleware.ts
export function middleware(request: NextRequest) {
const token = request.cookies.get('session');
const { pathname } = request.nextUrl;
// Protected routes
if (pathname.startsWith('/dashboard') && !token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}Error Boundaries#
// app/dashboard/error.tsx
'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-storeorrevalidateexplicitly. - 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.
More in Frontend
View all →Frontendv1.0.0
Component Design Systems
Building and maintaining scalable component libraries, design tokens, accessibility, and cross-team collaboration patterns
design-systemscomponentsreact
Frontendv1.0.0
React Patterns
Component patterns, hooks, state management, and performance optimization for React applications
reacthooksstate-management
Frontendv1.0.0
State Management
Modern frontend state management patterns, tools, architecture decisions, and scalability patterns
state-managementreactzustand