# What You’ll Build in This Guide#
You’ll implement a React frontend architecture feature-based folder strategy that scales past the first 3 to 5 features without turning into a maze of shared utilities and circular imports.
You’ll learn where to place API clients, UI primitives, domain logic, tests, and cross-cutting concerns in both React and Next.js. You’ll also get dependency rules you can enforce with ESLint, plus examples you can copy into your codebase.
This structure is designed for teams where:
- The app will grow beyond one developer.
- Multiple features ship in parallel.
- You want refactors to be localized instead of “touch 40 files across 10 folders”.
# Why Feature-Based Architecture Wins at Scale#
Folder structure becomes architecture the moment you have:
- more than 10 screens,
- more than 2 developers,
- more than 1 integration,
- more than 1 release per week.
A feature-based approach reduces coordination cost because code changes tend to stay within a module that matches how the product is discussed: onboarding, billing, search, checkout, account settings.
The industry trend backs this up. The 2024 State of JS survey reports React remains the most used UI library, and the most common pain reported by React teams is maintainability and complexity as apps grow. Feature-based boundaries don’t eliminate complexity, but they contain it.
If you also care about consistent UI primitives and scalable component design, pair this guide with React component architecture for scalable design systems.
# Core Principles: Boundaries, Dependencies, and Public APIs#
A feature-based architecture works only if boundaries are real. That means you define:
- 1What each module owns
- 2What it is allowed to import
- 3What it exposes publicly
Principle 1: Modules own business capability, not just UI#
A feature module should include:
- route-level UI (pages, screens)
- feature state and orchestration
- domain rules for that feature
- feature API calls
- feature-specific components
- feature tests
If you keep domain logic in random shared utilities, you recreate a monolith, just with more folders.
Principle 2: Dependencies flow one way#
Your structure should prevent a “grab bag” shared layer that everything depends on and everything mutates.
A simple rule that scales well:
- shared depends on nothing
- entities depends on shared
- features depends on entities and shared
- app composes features and global providers
This is close to well-known patterns like feature-sliced design, but simplified so it stays pragmatic.
Principle 3: Every module exports a public API#
If other modules import deep paths, boundaries are imaginary. You want imports like:
import { CheckoutPage } from "@/features/checkout";
import { formatMoney } from "@/shared/lib/money";and not:
import { formatMoney } from "@/shared/lib/money/formatMoney";
import { CheckoutForm } from "@/features/checkout/ui/forms/CheckoutForm";Deep imports bypass decisions and create “spaghetti coupling”.
🎯 Key Takeaway: A folder structure is only architecture if you enforce dependency direction and public exports.
# Recommended Folder Structure for React and Next.js#
This structure supports both:
- React SPA with React Router
- Next.js App Router with React Server Components
Use a src root and keep Next.js app at the project root.
High-level layout#
| Layer | Responsibility | Can import | Cannot import |
|---|---|---|---|
| app | Route composition, providers, bootstrapping | features, entities, shared | feature internals |
| features | User-facing capabilities, orchestration | entities, shared | other feature internals |
| entities | Core domain models and reusable entity UI | shared | features, app |
| shared | UI primitives, libs, config, base API client | nothing (or only npm deps) | app, features, entities |
| tests | Cross-feature test utilities, fixtures | shared | feature internals (ideally) |
Example folder tree#
src/
app/
providers/
routes/
features/
auth/
checkout/
search/
entities/
user/
product/
order/
shared/
api/
ui/
lib/
config/
types/
tests/
fixtures/
helpers/For Next.js App Router, you will typically have:
app/
(marketing)/
(app)/
api/
src/
features/
entities/
shared/The Next.js app directory is framework-owned routing. Your actual product code lives in feature modules under src.
ℹ️ Note: In Next.js, avoid putting business logic directly in
approutes. Treat route files as composition glue and keep logic insrc/featuresso it remains portable and testable.
# How to Design Feature Modules#
A feature module should feel like a small package with its own public surface.
Feature module template#
src/features/checkout/
index.ts
model/
useCheckout.ts
checkoutMachine.ts
selectors.ts
api/
checkoutApi.ts
types.ts
ui/
CheckoutPage.tsx
CheckoutForm.tsx
components/
PaymentMethodPicker.tsx
lib/
pricing.ts
validation.ts
tests/
checkout.test.ts
pricing.test.tsWhat goes where#
| Folder | Put here | Example |
|---|---|---|
| api | Feature-specific API calls and DTO mapping | createOrder, applyCoupon |
| model | State, hooks, orchestrators | useCheckout, XState machines |
| ui | Screens and feature components | CheckoutPage, CheckoutForm |
| lib | Pure functions used inside the feature | validateAddress, calcTotals |
| tests | Feature tests close to code | pricing.test.ts |
| index.ts | Public exports only | export { CheckoutPage } |
Feature public API example#
// src/features/checkout/index.ts
export { CheckoutPage } from "./ui/CheckoutPage";
export { useCheckout } from "./model/useCheckout";
export type { CheckoutDraft } from "./model/types";Only export what other modules should depend on. Everything else stays internal.
⚠️ Warning: If features import other features directly, you’ll quickly get cycles like
checkout -> auth -> cart -> checkout. Prefer cross-feature communication through shared abstractions, or lift orchestration toapp.
# Where API Clients Belong (Without Creating a Global God Module)#
Most teams start with src/api and end up with 200 files and unclear ownership. Instead, split “transport” from “endpoints”.
Shared API transport#
Keep a minimal HTTP client in shared/api. It should be boring and stable.
src/shared/api/
httpClient.ts
errors.ts
authHeader.ts// src/shared/api/httpClient.ts
export async function http<T>(
input: string,
init?: RequestInit
): Promise<T> {
const res = await fetch(input, {
...init,
headers: {
"content-type": "application/json",
...(init?.headers ?? {}),
},
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
return (await res.json()) as T;
}Feature endpoints and mapping#
Endpoints and DTO mapping belong to the feature because they change with feature requirements.
// src/features/checkout/api/checkoutApi.ts
import { http } from "@/shared/api/httpClient";
export type CreateOrderInput = {
items: Array<{ productId: string; qty: number }>;
};
export type CreateOrderResponse = { orderId: string };
export function createOrder(input: CreateOrderInput) {
return http<CreateOrderResponse>("/api/orders", {
method: "POST",
body: JSON.stringify(input),
});
}This avoids a central api folder becoming a bottleneck and keeps API changes localized.
Next.js note: server-only API calls#
If you are using App Router and want server-side fetching, move server-only API modules into a server folder and avoid client imports.
src/features/orders/
server/
ordersService.ts
api/
types.tsThen call ordersService from server components, route handlers, or server actions.
For deeper patterns around RSC, see React Server Components guide.
# UI Layers: Primitives vs Feature UI vs Entity UI#
Your UI layer should prevent two common problems:
- 1Copy-pasted UI primitives across features
- 2A “components” folder that turns into a dumping ground
Shared UI primitives#
These are low-level building blocks: Button, Input, Modal, Tabs, Skeleton, Toast. They must be:
- styling-consistent
- accessible by default
- dependency-light
src/shared/ui/
button/
Button.tsx
Button.test.tsx
index.ts
input/
modal/
index.tsA good rule: if a component has business meaning like CheckoutSummary, it is not shared UI.
Entity UI#
Entity modules are reusable because they represent core domain concepts: user, product, order. They often include “entity widgets” like UserAvatar, ProductCard, OrderStatusBadge.
src/entities/product/
model/
types.ts
ui/
ProductCard.tsx
PriceTag.tsx
index.tsEntity UI is the glue that keeps features consistent without pushing everything into shared.
Feature UI#
Feature UI composes shared UI and entity UI into outcomes: forms, screens, flows.
src/features/search/ui/
SearchBar.tsx
SearchResults.tsx
SearchPage.tsx💡 Tip: Run a design review when introducing new shared UI primitives. Shared components are expensive to change later because they accumulate many consumers. Use a checklist like React design review checklist for performance, accessibility, maintainability.
# Domain Logic: Keep It Pure, Close to the Feature, and Testable#
When domain rules are scattered across UI components, you get:
- duplicated logic
- fragile changes
- hard-to-test behavior
Where domain logic should live#
- Feature-specific domain rules stay inside
features/<feature>/liborfeatures/<feature>/model. - Cross-feature, stable domain primitives can go under
entities/<entity>/model. - Truly generic utilities go under
shared/lib.
Example: pricing logic in checkout#
// src/features/checkout/lib/pricing.ts
export function calcTotals(
subtotalCents: number,
discountCents: number,
taxRate: number
) {
const discounted = Math.max(0, subtotalCents - discountCents);
const taxCents = Math.round(discounted * taxRate);
return {
subtotalCents,
discountCents,
taxCents,
totalCents: discounted + taxCents,
};
}This can be unit-tested without rendering React, which keeps tests fast and reliable.
# Next.js Specifics: App Router, RSC, and Client Boundaries#
Next.js adds constraints that actually help architecture, if you lean into them.
Route files as composition only#
Keep app routes thin:
- read params
- call feature entry components
- wire providers
// app/(app)/checkout/page.tsx
import { CheckoutPage } from "@/features/checkout";
export default function Page() {
return <CheckoutPage />;
}Avoid placing API calls, validation rules, and transformation code in the route file.
Separating client and server code#
A practical convention:
| Folder | Runs where | Typical content |
|---|---|---|
features/x/ui | client by default | interactive components |
features/x/server | server only | DB-backed services, secret API calls |
features/x/api | either | DTO types, shared mapping |
If you need a client component, mark it explicitly at the top of the file.
// src/features/checkout/ui/CheckoutForm.tsx
"use client";
import { useCheckout } from "../model/useCheckout";
export function CheckoutForm() {
const { submit, isLoading } = useCheckout();
return (
<button onClick={submit} disabled={isLoading}>
Place order
</button>
);
}This keeps the server and client boundary explicit. It prevents accidental bundling of server-only dependencies into the browser.
# Dependency Rules You Can Enforce#
Rules prevent regressions when the codebase grows and new developers join.
Practical dependency rules#
- 1
sharedimports nothing from your app code. - 2
entitiescan import fromsharedonly. - 3
featurescan import fromentitiesandshared. - 4
appcan import from everything, but other layers should not import fromapp. - 5Features should not import internals of other features. Only public exports.
ESLint rule example (import restrictions)#
This is a minimal example using ESLint core rules. Many teams also add eslint-plugin-boundaries, but even core rules are a good start.
// .eslintrc.cjs
module.exports = {
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@/features/*/*"],
message: "Import from a feature public API only, not internal paths.",
},
{
group: ["@/app/*"],
message: "Do not import from app layer; app is composition only.",
},
],
},
],
},
};This prevents deep imports like @/features/checkout/ui/CheckoutForm from other modules, pushing developers to export from @/features/checkout.
# Testing Strategy: What to Test and Where to Put It#
A scalable testing setup avoids over-reliance on slow UI tests.
Recommended test placement#
| Test type | Location | What it covers | Fast? |
|---|---|---|---|
| Unit tests | features/*/lib or entities/*/model | pure logic and domain rules | Yes |
| Component tests | shared/ui or features/*/ui | component behavior, a11y, states | Medium |
| Integration tests | features/*/tests | feature flows with mocked API | Medium |
| E2E tests | e2e/ | real user journeys | No |
Example: unit test for pricing#
// src/features/checkout/lib/pricing.test.ts
import { calcTotals } from "./pricing";
test("calcTotals computes total with discount and tax", () => {
const r = calcTotals(10000, 2000, 0.25);
expect(r.totalCents).toBe(10000);
expect(r.taxCents).toBe(2000);
});Keep unit tests close to the logic. When you refactor, tests move with the code and remain discoverable.
# Common Scalability Pitfalls and How to Avoid Them#
Pitfall 1: The shared folder becomes a junk drawer#
If shared includes feature-specific code, everything starts depending on it and refactors become risky.
Fix: shared contains only primitives and truly generic libs. Move business meaning into entities or features.
Pitfall 2: “components” as the only organizing concept#
A top-level components folder hides business context. You end up searching for “where is the checkout form” and guessing.
Fix: feature modules own their UI. Shared UI is only primitives.
Pitfall 3: Deep imports that bypass boundaries#
Deep imports are a leading indicator of future coupling.
Fix: enforce public APIs via index.ts files and lint rules.
Pitfall 4: Duplicated API mapping logic#
When every screen maps the same DTO differently, bugs appear during API changes.
Fix: map DTOs once inside feature api modules or entity model mappers.
# A Maintainable Example: Putting It All Together#
Assume you’re building an e-commerce app with search, checkout, and account.
shared/uiprovides Button, Input, Dialog.entities/productprovides ProductCard, types, and formatting.features/searchowns search query state, debouncing, search API, and results UI.features/checkoutowns pricing logic, cart submission, and payment flow.appwires routing, providers, and feature composition.
This keeps changes local:
- Changing pricing rules affects
features/checkout/lib/pricing.tsand tests. - Changing how a product is displayed affects
entities/product/ui/ProductCard.tsx. - Changing Button styling affects
shared/ui/button.
When you do an architectural review, you can scan feature boundaries quickly, then apply a checklist like React design review checklist for performance, accessibility, maintainability.
# Key Takeaways#
- Organize by features and entities, not by generic “components”, and keep route files as thin composition.
- Put a thin HTTP transport in
shared/api, and keep endpoints and DTO mapping inside each feature. - Separate UI into shared primitives, entity UI, and feature UI to avoid a bloated shared layer.
- Enforce one-way dependencies and feature public APIs using
index.tsexports and ESLint import restrictions. - Keep domain logic pure and test it with fast unit tests close to the code, then add integration and E2E only where needed.
# Conclusion#
A React frontend architecture feature-based setup succeeds when boundaries are enforceable, imports are intentional, and business logic stays close to the features that own it. If you want help applying this structure to an existing codebase, or you need a Next.js App Router architecture that’s ready for RSC, accessibility, and performance reviews, reach out to Samioda for an architecture audit and a concrete refactor plan.
FAQ
Founder & Senior Developer at Samioda. 8+ years building React, Next.js, Flutter and n8n automation solutions for clients across Europe.
More in Web Development
All →Observability for Next.js App Router in 2026: Sentry, OpenTelemetry, Traces, and Actionable Alerts
End-to-end setup for Next.js logging, monitoring, and tracing with Sentry and OpenTelemetry across Server Actions, Route Handlers, and Edge versus Node runtimes—plus dashboards, alert thresholds, and session-to-trace correlation.
Next.js App Router UX Patterns: Error Boundaries, Loading UI, and Streaming Done Right
A practical guide to resilient UX in Next.js App Router: route segment structure, error.tsx, loading.tsx, not-found.tsx, and Suspense streaming patterns for partial rendering, safer data fetching, and fewer layout shifts.
Building a Design System in Next.js with Radix UI, Tailwind, and Storybook: End-to-End Guide for 2026
A practical, production-ready approach to building and maintaining a Next.js design system using Radix UI for accessibility, Tailwind for styling, and Storybook for documentation, testing, and versioned releases.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
React Query at Scale: Cache Invalidation, Pagination, and Mutation Patterns for Real Apps
React Query cache invalidation best practices for real-world apps: scalable query key design, invalidation strategy, optimistic updates, infinite queries, and background refetching in Next.js App Router.
Next.js App Router UX Patterns: Error Boundaries, Loading UI, and Streaming Done Right
A practical guide to resilient UX in Next.js App Router: route segment structure, error.tsx, loading.tsx, not-found.tsx, and Suspense streaming patterns for partial rendering, safer data fetching, and fewer layout shifts.
Building a Design System in Next.js with Radix UI, Tailwind, and Storybook: End-to-End Guide for 2026
A practical, production-ready approach to building and maintaining a Next.js design system using Radix UI for accessibility, Tailwind for styling, and Storybook for documentation, testing, and versioned releases.