Clean Architecture in a Next.js Codebase
September 02, 2025#architecture#next.js#typescript

The problem with "just put it in components"
Components start small and honest, then attract every dependency in the app. After six months the checkout flow has a fetch inside the button handler and a date-formatting util in the page file. It works — until it stops working, and nothing can be tested in isolation.
The three layers I actually keep
- Presentation — components, hooks, and styling. Knows about "screen", not "world".
- Domain — types, rules, and pure functions:
Cart,isEligibleForVoucher(), price math. Zero imports from Next.js. - Data — API clients and queries. The only layer allowed to touch
fetch.
// domain/cart.ts — no React, no fetch.
export function isEligibleForVoucher(
cart: Cart,
voucher: Voucher,
): boolean {
return cart.total >= voucher.minSpend && voucher.unusedBy <= cart.userId;
}
The rule that keeps it honest
Dependencies point inward. Domain imports nothing; presentation imports domain; data implements the contracts domain defines. The moment a pull request violates that, the review stops. It is one sentence to write and the entire discipline to enforce.
Layers are not folders. A folder of
domain/,data/,components/with imports crossing in both directions is just a nicer-looking mess.
***