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 (1)

Collapse
 
mrakdon profile image
Mrakdon.com

nice insight