Frontend Development

Building Scalable Frontend Architecture with Next.js

Next.js has become the go-to framework for building production-ready React applications. However, scaling a Next.js application requires careful architectural decisions. This article shares patterns and practices for building maintainable, scalable frontend architectures.

Project Structure

A well-organized project structure is the foundation of scalability:

project-root/
├── app/                    # Next.js App Router
│   ├── (auth)/            # Route groups
│   │   ├── login/
│   │   └── register/
│   ├── (dashboard)/
│   │   ├── layout.tsx
│   │   └── dashboard/
│   ├── api/               # API routes
│   └── layout.tsx
├── components/
│   ├── ui/                # Reusable UI components
│   ├── features/          # Feature-specific components
│   └── layouts/           # Layout components
├── lib/                   # Utilities and helpers
├── hooks/                 # Custom React hooks
├── types/                 # TypeScript types
└── styles/               # Global styles

App Router vs Pages Router

Next.js 13+ introduced the App Router with significant improvements:

typescript
// ✅ App Router - Server Component by default
// app/dashboard/page.tsx
async function DashboardPage() {
  // Fetch data directly in component
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store', // or 'force-cache' for static
  }).then(res => res.json());

  return (
    <div>
      <h1>Dashboard</h1>
      <DashboardContent data={data} />
    </div>
  );
}

// ✅ Client Component when needed
'use client';

import { useState } from 'react';

function InteractiveComponent() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

State Management Patterns

typescript
// ✅ Server State with React Query
'use client';

import { useQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user', userId],
    queryFn: async () => {
      const res = await fetch(`/api/users/${userId}`);
      if (!res.ok) throw new Error('Failed to fetch');
      return res.json();
    },
    staleTime: 5 * 60 * 1000, // 5 minutes
  });

  if (isLoading) return <Spinner />;
  if (error) return <ErrorMessage />;
  return <UserDetails user={data} />;
}

// ✅ Client State with Zustand
import { create } from 'zustand';

interface Store {
  count: number;
  increment: () => void;
  decrement: () => void;
}

const useStore = create<Store>((set) => ({
  count: 0,
  increment: () => set((state) => ({ count: state.count + 1 })),
  decrement: () => set((state) => ({ count: state.count - 1 })),
}));

Data Fetching Strategies

Next.js offers multiple data fetching options:

typescript
// ✅ Server Component data fetching
async function ProductPage({ params }: { params: { id: string } }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 3600 }, // Revalidate every hour
  }).then(res => res.json());

  return <ProductDetails product={product} />;
}

// ✅ API Routes
// app/api/users/route.ts
export async function GET(request: Request) {
  const users = await db.users.findMany();
  return Response.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  const user = await db.users.create({ data: body });
  return Response.json(user, { status: 201 });
}

Performance Optimization

typescript
// ✅ Image Optimization
import Image from 'next/image';

function OptimizedImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={600}
      placeholder="blur"
      blurDataURL="data:image/..."
      priority // For above-the-fold images
    />
  );
}

// ✅ Dynamic Imports
import dynamic from 'next/dynamic';

const HeavyComponent = dynamic(() => import('./HeavyComponent'), {
  loading: () => <Spinner />,
  ssr: false, // Disable SSR if needed
});

function Page() {
  return <HeavyComponent />;
}

Conclusion

Building scalable frontend architecture with Next.js requires thoughtful planning and consistent patterns. By organizing code effectively, choosing the right tools, and following best practices, you can create applications that scale gracefully as your team and user base grow.