DEV Community

Cover image for Modular Architecture Example for Next.js 16
Rama Can
Rama Can

Posted on

Modular Architecture Example for Next.js 16

Modular Architecture Example for Next.js 16

Insight: A well‑structured codebase reduces cognitive load, accelerates onboarding, and makes future scaling painless.

Introduction

Next.js 16 introduces server‑components, edge‑runtime, and a more flexible routing system. While these features unlock powerful patterns, they also raise the question: how do we keep the project maintainable as it grows? This post walks you through a modular architecture that isolates features, promotes reuse, and aligns with the latest Next.js conventions.

What You Will Learn

  • How to organize feature modules and shared layers.
  • Best practices for routing, API routes, and edge functions.
  • Configuration tricks for incremental static regeneration and bundle analysis.
  • A ready‑to‑copy file‑tree and sample code snippets.

Project Layout Overview {#project-layout}

my-next-app/
├─ src/
│  ├─ app/                # App Router (pages, layout, loading, error)
│  │  ├─ (auth)/
│  │  │  └─ page.tsx
│  │  └─ (dashboard)/
│  │     └─ page.tsx
│  ├─ features/           # Feature‑centric modules
│  │  ├─ cart/
│  │  │  ├─ components/
│  │  │  ├─ hooks/
│  │  │  └─ api/
│  │  └─ product/
│  │     ├─ components/
│  │     └─ api/
│  ├─ shared/             # Reusable utilities, UI, types
│  │  ├─ ui/
│  │  ├─ lib/
│  │  └─ types/
│  └─ config/             # Next.js and third‑party configs
│     └─ next.config.mjs
└─ public/
   └─ images/
Enter fullscreen mode Exit fullscreen mode

Why This Layout?

  • Feature folders (src/features/*) keep all code related to a domain together – components, hooks, and API calls live side‑by‑side.
  • Shared layer (src/shared/*) houses generic UI primitives and utilities, avoiding duplication.
  • App Router (src/app) follows the new file‑system routing, allowing server components where appropriate.

Feature Modules {#feature-modules}

Each feature is a self‑contained module exposing a public API through an index.ts barrel file.

// src/features/cart/index.ts
export { CartProvider } from './hooks/useCart';
export { CartButton } from './components/CartButton';
export { cartApi } from './api/cartApi';
Enter fullscreen mode Exit fullscreen mode

Using a Feature in a Page

// src/app/(dashboard)/page.tsx
import { CartButton } from '@/features/cart';
import { ProductList } from '@/features/product';

export default function DashboardPage() {
  return (
    <section>
      <h1>Dashboard</h1>
      <ProductList />
      <CartButton />
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tip: Exporting only the public surface keeps internal implementation details private and enables tree‑shaking.

Shared Utilities {#shared-utilities}

The src/shared/lib folder contains helpers that are agnostic to any feature.

// src/shared/lib/fetcher.ts
export async function fetcher<T>(url: string, init?: RequestInit): Promise<T> {
  const res = await fetch(url, init);
  if (!res.ok) throw new Error(`Failed to fetch ${url}`);
  return (await res.json()) as T;
}
Enter fullscreen mode Exit fullscreen mode

You can now reuse fetcher across feature APIs:

// src/features/product/api/productApi.ts
import { fetcher } from '@/shared/lib/fetcher';

export const getProducts = () => fetcher<Product[]>('/api/products');
Enter fullscreen mode Exit fullscreen mode

Routing & API Layers {#routing-api}

Next.js 16 encourages Route Handlers for server‑only logic. Place them under app/api.

// src/app/api/cart/route.ts
import { NextResponse } from 'next/server';
import { cartService } from '@/features/cart/api/cartService';

export async function POST(request: Request) {
  const { productId, quantity } = await request.json();
  const result = await cartService.add(productId, quantity);
  return NextResponse.json(result);
}
Enter fullscreen mode Exit fullscreen mode

Edge‑Ready Handlers

Add the runtime: 'edge' export to run the handler at the edge.

// src/app/api/health/route.ts
export const runtime = 'edge';
export async function GET() {
  return new Response('OK', { status: 200 });
}
Enter fullscreen mode Exit fullscreen mode

Configuration & Build Optimizations {#config-build}

A minimal next.config.mjs that enables bundling analysis and incremental static regeneration defaults:

// src/config/next.config.mjs
import { defineConfig } from 'next';

export default defineConfig({
  reactStrictMode: true,
  experimental: {
    appDir: true,
  },
  images: {
    remotePatterns: [{ hostname: 'cdn.example.com' }],
  },
  webpack: (config, { dev, isServer }) => {
    if (!dev) {
      config.optimization.minimize = true;
    }
    return config;
  },
});
Enter fullscreen mode Exit fullscreen mode

Run next build && next export to generate a fully static site, or drop the output: 'standalone' flag for a server‑only deployment.

Testing Strategy {#testing}

Keep tests module‑scoped using the same folder structure.

src/
├─ features/
│  └─ cart/
│     └─ __tests__/
│        └─ cart.test.ts
Enter fullscreen mode Exit fullscreen mode

Use Jest with React Testing Library for component tests and Playwright for end‑to‑end scenarios.

Conclusion

By adopting a feature‑first modular architecture, you gain:

  • Clear ownership boundaries.
  • Faster builds thanks to better tree‑shaking.
  • Easier onboarding for new developers.
  • Seamless migration to future Next.js releases.

Next step: Clone the starter repo, replace the placeholder components with your business logic, and ship your first production‑ready Next.js 16 module today.


Happy coding!

Top comments (0)