Next.js Frontend Structure

Updated 8 July 2026

Next.js 15 uses the App Router with file-based routing. This section covers how to structure your frontend, use Server and Client Components, and implement data fetching patterns.

App Router Architecture

Next.js 15 uses the App Router with file-based routing. The file structure in the app directory determines the routes:

Route Groups

Route groups (folders in parentheses) organize routes without affecting the URL:

Route Structure

app/
├── (admin)/admin/        # /admin/*
│   ├── products/         # /admin/products
│   ├── categories/       # /admin/categories
│   └── tags/            # /admin/tags
└── (public)/            # Public routes
    ├── page.jsx         # / (homepage)
    ├── shop/            # /shop
    ├── product/[slug]/  # /product/slug
    ├── cart/            # /cart
    ├── checkout/        # /checkout
    └── account/         # /account

Server Components vs Client Components

Next.js 15 distinguishes between Server Components (default) and Client Components:

Server Components (Default)
Client Components

Server Component Example

import { WooCommerceAPI } from "@/lib/woocommerce"; // Import WooCommerce API

export default async function ShopPage() { // Server component to fetch products
  const { products } = await WooCommerceAPI.getProducts();
  
  return (
    
      {products.map(product => (
        
      ))}
    
  );
}

Benefits: Server Components are rendered on the server, so the HTML is sent directly to the browser. This improves SEO and initial page load performance.

Client Component Example

"use client";

import { useCart } from "@/contexts/CartContext"; // Import Cart context

export default function AddToCartButton({ productId }) { // Client component to add to cart
  const { addToCart } = useCart(); // Get add to cart function from Cart context
  
  return (
     addToCart(productId, 1)}> // Add to cart with quantity 1 to the product with the given productId
      Add to Cart
    
  );
}

Data Fetching Patterns

Next.js 15 provides multiple ways to fetch data. Choose the pattern that best fits your use case:

1. Server-Side Fetching (Recommended)

export default async function ProductPage({ params }) { // Server component to fetch product by slug
  const product = await WooCommerceAPI.getProductBySlug(params.slug); // Get product by slug
  if (!product) notFound(); // If product not found, show 404 page
  return ; // Return product details component
}

2. API Routes + Client Fetching

"use client";

useEffect(() => {
  fetch("/api/products")
    .then(res => res.json())
    .then(data => setProducts(data));
}, []);

3. Server Actions (Next.js 15)

// app/actions.js
"use server";
export async function getProducts() { // Server action to fetch products
  return await WooCommerceAPI.getProducts();
}

// In component
const data = await getProducts();

Dynamic Routes

Use square brackets for dynamic route segments:

// app/product/[slug]/page.jsx
export default async function ProductPage({ params }) { // Server component to fetch product by slug
  const product = await WooCommerceAPI.getProductBySlug(params.slug); // Get product by slug
  if (!product) notFound(); // If product not found, show 404 page
  return ;
}

Layouts

Layouts wrap pages and persist across navigation:

// app/layout.js
export default function RootLayout({ children }) { // Root layout to wrap pages and persist across navigation
  return (
            {children}
  );
}

Metadata and SEO

export async function generateMetadata({ params }) {
  const product = await WooCommerceAPI.getProductBySlug(params.slug); // Get product by slug
  if (!product) notFound(); // If product not found, show 404 page
  return {
    title: product.name, // Set product name as title
    description: product.short_description, // Set product short description as description
    description: product.short_description,
  };
}

Loading and Error States

// loading.jsx
export default function Loading() { // Loading component to show loading state
  return Loading...;
}

// error.jsx
"use client";
export default function Error({ error, reset }) { // Error component to show error state
  return (
    
      {error.message}
      Try again // Try again button to reset the error
    
  );
}

Best Practices