Derived Modular Arch
Guides

Next.js

DMA on App Router: Server Components, Client boundary, providers.

This example shows how DMA fits the RSC boundary of Next.js App Router. Not a full runnable app in v1 — enough source for dma check and ESLint.

Sources: examples/next-app.

Tree

src/
├── app/
│   ├── layout.tsx       # Server — shell + <Providers>
│   ├── page.tsx         # Server — thin route
│   ├── providers.tsx    # Client — SessionProvider
│   └── globals.scss
├── features/
│   ├── catalog/         # catalog.store + public/catalog-page
│   ├── checkout/
│   ├── search/
│   └── profile.tsx      # stage 0
├── services/
│   ├── cart/public/
│   └── session/public/
└── shared/
    ├── domain/
    ├── lib/
    └── ui/

Direction: app/pages/routes → features → services → shared. Features do not import each other.

Server vs Client

Next.js defaults to Server Components. DMA keeps the split at composition root vs feature public entries:

FileBoundaryWhy
layout.tsxServerHTML shell, no hooks
page.tsxServerMounts only */public/*
providers.tsxClient ("use client")Context needs client boundary
catalog-page.tsxClientonClick, store
button.tsxServer-safeWithout "use client" — pulled into client bundle via feature
app/layout.tsx          (Server)
  └── app/providers.tsx (Client)
        └── app/page.tsx        (Server)
              ├── features/catalog/public/catalog-page.tsx   (Client)
              ├── features/search/public/search-page.tsx     (Client)
              └── features/profile.tsx                       (Client)

Pattern: page.tsx stays a thin server composition root. Interactivity lives in feature public/ entries. Do not import *.store.ts from page.tsx.

Thin page.tsx

import { CatalogPage } from "@/features/catalog/public/catalog-page";
import { CheckoutPage } from "@/features/checkout/public/checkout-page";
import { Profile } from "@/features/profile";
import { SearchPage } from "@/features/search/public/search-page";
// ❌ never in app/
import { catalogStore } from "@/features/catalog/catalog.store";

Providers in app

Cross-cutting context (session) — in app/providers.tsx:

"use client";
import { SessionProvider } from "@/services/session/public/session-provider";

export function Providers({ children }) {
  return <SessionProvider>{children}</SessionProvider>;
}

profile.tsx reads useSession() — provider is mounted in app, not in a feature.

Services

ModuleConsumersRole
cartcatalog, checkoutaddToCart, cartTotal
sessionprofile (via provider)SessionProvider

services/ appeared because two modules import cart — promotion from colocation.

Styles

KindWhereExample
Globalapp/globals.scssreset, shell
SCSS modulenext to public/ pagecatalog-page.module.scss
CSS modulenext to public/ pagecheckout-page.module.css

Global styles — only from layout.tsx. Features do not import globals.

Tooling

bun run dma-check   # dma check .
bun run lint        # eslint + compositionRoots: ["app", "pages", "routes"]
bun run test

ESLint catches file-scoped errors (store from page.tsx). Full graph — only dma check.

What's next

On this page