Stop fighting your folder structure — a scalable frontend architecture for React, Next.js, and Angular
By Anas Arafeh
Every frontend team I've worked with eventually hits the same wall. The app grows. Five modules become twenty. Three developers become fifteen. And suddenly, nobody knows what can break when they change a component.
"Why did editing the user profile card break the checkout flow?"
"Which modules does this shared table component affect?"
"Is this developer even supposed to be touching the dashboard module?"
If you've asked any of these questions, your architecture failed you. Not because your developers are bad. Because your architecture never told them where things go.
I've spent years refining a solution. I call it FFD — Frontend For Dummies. Not because it's for dummies — or maybe it is. Because it's a frontend architecture approach designed to be simple enough that anyone can understand it by looking at a screen.
The One Rule
Organize your code the way the screen is organized.
Open your app. Look at any page.
You see sections. Each section has areas — a chart area, a table area, a form area. Some sections share a similar business domain. Some don't. The screen already tells you the hierarchy.
FFD says: your code should mirror what the user sees.
Module
├── Page → The screen the user is looking at
│ └── Section → A distinct region of the page
│ └── Area → A functional unit within that region
│ └── Segment (optional) → Splits large Areas
That's it. That's the whole thing. Everything else is rules about what each layer can and cannot do.
The Layers
Page — "The Container"
The Page is the entry point. It arranges Sections on the screen. It does nothing else.
- No business logic.
- No API calls (in CSR frameworks).
- No state management.
Think of it as the table of contents for a screen. "Here goes the overview section. Here goes the table section. Here goes the chart section."
In Next.js, the Page can optionally prefetch data server-side and pass it down. But Sections still own their data — prefetching is just an optimization.
Section — "The Orchestrator"
The Section arranges Areas. It owns the state for everything below it.
The Section calls APIs that multiple Areas share. If three Areas need product data, the Section fetches it once and distributes it through its provider.
No business logic lives here. Business logic is for Areas. Each Section has its own state management. If two Sections need shared state, lift it to the module level — but shared state is the exception, not the rule.
Area — "The Doer"
The Area is where the real work happens. Business logic. Complex interactions. Local state. API calls specific to this Area — like submitting a form or fetching auto-complete suggestions.
Areas never receive props from their parent Section. They subscribe to the Section's context. This forces a clean, testable boundary: wrap an Area in its provider with mock data, and you can test it in isolation.
Segment — Optional, for Large Areas
When an Area gets too large, split it into Segments — mini-Areas that group related interactions. A 30-field form becomes three Segments of 10 fields each. For example, our sample product-detail page splits the purchase area into price-summary, delivery-info, cart-actions, and protection-plans — each a self-contained segment. The Area still owns the submission logic. Same rules as Areas: no props from parents, context-driven. Not every Area needs Segments — they're a tool for when complexity demands it.
The Rules That Make It Work
1. Same-Level Isolation
Module A ──X── Module B Never (same level)
Section X ──X── Section Y Never (same level)
Area 1 ──X── Area 2 Never (same level)
Module ──✓── (shared)/ Allowed (higher level)
Module ──✓── services/ Allowed (higher level)
Section ──✓── helpers/ Allowed (higher level)
Section ──✓── components/ Allowed (higher level)
Area ──✓── theme/ Allowed (higher level)
Area ──✓── services/ Allowed (higher level)
Entities on the same level never reference each other. A module cannot import from another module. A section cannot import from another section. You can only reach up — to shared resources, helpers, services, and theme components.
This single rule changes everything about code review. When you see a PR touch files inside (modules)/product-detail/, you know with near certainty that only the Product Detail module is affected.
2. Duplicate by Default, Share With Purpose
FFD doesn't ban sharing. It has dedicated folders for it — theme/, components/, helpers/, and services/ are all meant to be shared. But when it comes to module-specific code, the default is duplication over premature abstraction.
Think of it this way: share what's genuinely universal (theme wrappers, API clients, utility functions). Duplicate what's specific to a feature (a product card, a user table, a checkout form). If the same component proves stable across multiple modules over time, promote it to components/. But don't start there.
Here's why the default matters:
Teams have varied skill levels. You cannot trust every developer to handle shared components correctly. When asked to add a feature, a developer's first instinct is to edit the component in front of them — not check all 15 places it's used.
Teams change, people leave. Where a shared component is used, how to split its logic, which pages to visit to verify changes — all of this lives in people's heads. When they leave, it's gone. And the next developer ships a bug because they didn't know Module X used the same component in a slightly different way.
Version 2.0 doesn't care about your shared code. When Module A gets a full redesign, every shared component it uses becomes a negotiation. "We can't change this button — Module B, C, and D use it too." With duplicated code, you redesign Module A and nothing else breaks.
What's always shared: Theme components (BaseBox, BaseButton, BaseCard, BaseTable), global utilities in helpers/, and API services in services/. These have no business logic and don't change per module.
What you duplicate by default: Module-specific components, section layouts, area-level widgets. Promote to components/ only after they've proven stable across multiple modules.
3. No Props in the Architectural Chain
Page → Section ❌ No props
Section → Area ❌ No props
Area → Segment ❌ No props
Delegate Component ✅ Props (injectable — passed to tables, menus, etc.)
Theme Component ✅ Props (regular components)
Shared Component ✅ Props (regular components)
This rule exists because if you give developers the option to use props between layers, they will. And suddenly you have two competing patterns: props here, context there, nobody knows which to use when. One rule. No exceptions. State flows through context — always.
The exception: Delegate Components. These are injectable slots — you pass them as props to shared components (tables, menus, carousels). The host calls the delegate with row data. Delegates are placed next to the Area or Section that defines them, not mixed in with private components.
4. Each File Screams Its Role
Every file ends with its layer type:
product-detail.page.tsx
product-showcase.section.tsx
product-gallery.area.tsx
cart-actions.segment.tsx
user.models.ts
status.enums.ts
users.api.ts
A reviewer sees product-gallery.area.tsx and knows instantly: "Area = business logic. No props from parents. Can call its own APIs. Test with a context wrapper."
5. Promote Cautiously
Used by 2+ Segments → promote to Area level. Used by 2+ Areas → promote to Section level. Used by 2+ Sections → promote to Module level. Used by 2+ Modules → consider components/, but default to duplication. The same rule applies to Delegate Components.
The Project Structure
src/
├── (modules)/ → Feature modules. Each Page = one module.
│ └── product-detail/
│ ├── product-detail.page.tsx
│ ├── state-management/
│ │ ├── product-showcase/
│ │ └── related-products/
│ ├── sections/
│ │ ├── product-showcase/
│ │ │ ├── product-showcase.section.tsx
│ │ │ └── areas/
│ │ │ ├── product-gallery/
│ │ │ ├── product-info/
│ │ │ │ └── delegate-components/
│ │ │ └── product-purchase/
│ │ │ └── segments/
│ │ │ ├── cart-actions/
│ │ │ ├── delivery-info/
│ │ │ ├── price-summary/
│ │ │ └── protection-plans/
│ │ └── related-products/
│ │ └── areas/
│ │ └── related-items/
│ ├── models/
│ ├── helpers/
│ ├── store/
│ └── components/
│
├── (shared)/ → Shared features (Header, Footer) — the examples include a full FFD-structured Amazon-style header
├── theme/components/ → UI wrappers (BaseBox, BaseButton, BaseCard, BaseTable)
├── components/ → Shared components (share cautiously)
├── services/ → API layer, split by backend domain
├── helpers/ → Global utilities
├── models/ → Global generic models
├── types/ → TS type overrides
└── store/ → Global constants
Each module can include whatever folder types it needs — models/, helpers/, components/, store/, state-management/, and more. Nothing is restricted.
Does This Actually Work?
I've used this architecture (and earlier versions of it) across:
- React
- Next.js
- Angular
- React Native
The framework changes. The architecture doesn't. Each framework has its own idiomatic way to implement state and providers — React uses Context + useReducer, Next.js uses server actions with client-side context, and Angular uses individual signals for fine-grained reactivity. FFD doesn't care which one you use.
Examples are available for React, Next.js, and Angular — all implementing the same Product Detail Page (Amazon-style) so you can compare how each framework handles the same architecture.
When to Skip FFD
FFD is built for growing applications with multiple developers. If you're a solo dev building a single-page app, you don't need this level of organization — any structure works at that scale. But once your team expands and the app grows, applying an architecture early keeps you out of the spiral.
How to Adopt FFD
Already have a large codebase? FFD can be adopted module by module. You don't need a full rewrite. Create your first (modules)/ folder, move one feature at a time, and let old code coexist outside the module structure. Enforce the hierarchy only for new modules. Over time, your codebase transitions without a single massive migration.
What FFD Doesn't Cover
FFD is scoped to code organization. It deliberately does not prescribe:
- Testing strategy (add
__tests__/wherever you need it) - Error handling patterns (code logic, not architecture)
- Performance optimization (implementation detail)
- CI/CD (project-specific)
- Styling methodology (use whatever you want — just keep styles next to their component)
The architecture tells you where things go. How you implement what's inside is up to you.
Try It
The complete architecture reference, working examples in React, Next.js, and Angular — all on GitHub:
Frontend-For-Dummies-Architecture on GitHub
- Read the full architecture doc (15 minutes)
- Browse working examples for your framework (React, Next.js, Angular) — all three implement the same Amazon-style product page so you can compare how each framework handles the architecture
- Create your first module following Page → Section → Area → Segment
Top comments (0)