HomeWorksExperimentsWriting

© 2025 Guo Ying. Made with midnight builds.

Back

Clean Architecture in a Next.js Codebase

September 02, 2025#architecture#next.js#typescript

Layer diagram

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

  1. Presentation — components, hooks, and styling. Knows about "screen", not "world".
  2. Domain — types, rules, and pure functions: Cart, isEligibleForVoucher(), price math. Zero imports from Next.js.
  3. 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.

***