How I Modularize Code in Large-Scale Next.js Projects

December 16, 2024⏱️ 4 min read

A detailed look at how I structure and modularize Next.js App Router projects using feature-based folders, reusable components, and clean architecture principles.

How I Modularize Code in Large-Scale Next.js Projects

How I Modularize Code in Large-Scale Next.js Projects

As projects grow, code that once felt manageable can quickly turn into a tangled mess — scattered files, repeated logic, and fragile components that break with the slightest change.

Over time, I've developed a clear approach to modularizing code in my large-scale Next.js projects, especially with App Router, TypeScript, and Tailwind CSS. The goal isn’t just to make things tidy — it’s to build something that’s scalable, navigable, and team-friendly, even if you’re currently a team of one.

🧱 Start with a Feature-First Mindset

I no longer organize code by type (e.g. components/, pages/, utils/). Instead, I group by feature or domain.

Example:

css

src/
├── features/
│   ├── checkout/
│   │   ├── CheckoutPage.tsx
│   │   ├── CourierPopup.tsx
│   │   └── payment/
│   │       ├── PaymentPopup.tsx
│   │       └── InvoicePopup.tsx
│   ├── profile/
│   │   ├── EditProfile.tsx
│   │   └── AddressEnhanced.tsx

This makes onboarding easier. When I (or someone else) needs to touch “checkout”, everything is in one place — logic, UI, state, and API hooks.

🧩 Break Components by Role, Not Size

Instead of thinking "big vs small components", I think in terms of responsibility:

  • ItemCard.tsx → presentational only

  • ItemDetail.tsx → display + logic

  • useItemData.ts → data fetching hook

  • ItemStore.ts → local Zustand store (if needed)

The result? Each file has a single purpose, and I know exactly where to look when something breaks.

"If you need to scroll more than 2 screens to understand a component, it’s probably doing too much."

🔁 Create Reusable Building Blocks

I maintain a global components/ folder for generic, reusable UI pieces:

mathematica

src/components/
├── Button.tsx
├── InputField.tsx
├── Modal.tsx
├── Screen.tsx

These are not tied to any business logic. They’re simple, consistent, and used across features.

Every time I find myself copying code from one screen to another, that’s a sign I need to extract a building block.

💡 Use Aliases to Stay Sane

Nothing kills DX like this:

tsx

import Button from '../../../../../../../components/Button'

That’s why I always configure path aliases in tsconfig.json:

json

{
  "paths": {
    "@/*": ["src/*"]
  }
}

Now I can write:

tsx

import Button from '@/components/Button'

Simple, readable, and portable.

🧠 Shared State? Choose Wisely

I use Zustand for local/global state when needed — but only if it actually simplifies things.

Pattern:

  • UI State → local component state.

  • Cross-component sync → Zustand store in src/stores/

  • Server state → Firestore or SWR/fetcher hooks in lib/

Avoid unnecessary complexity. Not everything needs global state.

🗃️ Example Folder Layout (Real Project)

From one of my actual projects:

pgsql

src/
├── app/└── (App Router pages)
├── components/
├── features/
│   └── checkout/
│       ├── CourierPopup.tsx
│       ├── PaymentPopup.tsx
├── lib/
│   └── firebase.ts
├── stores/
│   └── useCartStore.ts
├── types/
│   └── index.ts

It’s not fancy — it’s predictable.

✨ Final Thought: Structure Is UX for Developers

The same way we design UI for users, we should design file structure for ourselves — the developer experience.

Good architecture isn't about complexity. It’s about making your future self say:

“Oh wow, this is easy to follow.”
Not:
“Who the heck wrote this?!”

By modularizing early and consistently, I’ve saved myself from burnout, confusion, and late-night debugging.

And best of all, I enjoy working with my own code.