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/
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';
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>
);
}
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;
}
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');
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);
}
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 });
}
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;
},
});
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
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)