Skip to main content

The Monolithic Component Anti-Pattern

When building applications with Next.js, the most common developer instinct is to place all application logic directly inside React components. This approach, known as an exclusively component-driven architecture, works for toy prototypes but rapidly devolves into a maintenance nightmare in real engineering projects.

In this lesson, we will analyze why overloaded components (Fat Components) degrade code maintainability and how Clean Architecture principles applied to the frontend solve this problem at its root.


1. Anatomy of a Spaghetti Component​

Consider a simple screen for managing physical exercise routines (a common module connected to a NestJS backend). In a naive Next.js approach, the ExercisePage.tsx file typically looks like this:

src/app/exercises/page.tsx (Anti-pattern: Fat Component)
'use client';

import { useState, useEffect } from 'react';

// Typing coupled directly to the backend JSON contract
interface ExerciseFromApi {
id: string;
exercise_name: string;
muscle_group: string | null;
difficulty_lvl?: number;
}

export default function ExercisePage() {
const [exercises, setExercises] = useState<ExerciseFromApi[]>([]);
const [loading, setLoading] = useState(true);
const [name, setName] = useState('');
const [muscle, setMuscle] = useState('');

// 1. Direct network access inside the component
useEffect(() => {
fetch('http://localhost:3000/api/exercises')
.then((res) => res.json())
.then((data) => {
setExercises(data);
setLoading(false);
})
.catch((err) => console.error(err));
}, []);

// 2. Business logic and invariants tangled with view events
const handleCreate = async () => {
if (name.trim().length < 3) {
alert('Name must be at least 3 characters');
return;
}

if (!['CHEST', 'BACK', 'LEGS', 'ARMS'].includes(muscle.toUpperCase())) {
alert('Invalid muscle group');
return;
}

// 3. Payload manually formatted to satisfy backend requirements
const payload = {
exercise_name: name,
muscle_group: muscle.toUpperCase(),
difficulty_lvl: 2,
};

const res = await fetch('http://localhost:3000/api/exercises', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});

const created = await res.json();
setExercises((prev) => [...prev, created]);
setName('');
};

// 4. Visual rendering with numerous Tailwind utility classes
return (
<div className="p-8 max-w-4xl mx-auto">
<h1 className="text-2xl font-bold mb-4">Exercise Management</h1>
<div className="flex gap-2 mb-6">
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
className="border p-2 rounded"
/>
<button onClick={handleCreate} className="bg-blue-600 text-white px-4 py-2 rounded">
Save
</button>
</div>

{loading ? (
<p>Loading exercises...</p>
) : (
<ul className="space-y-2">
{exercises.map((item) => (
<li key={item.id} className="p-3 border rounded shadow-sm">
<span className="font-semibold">{item.exercise_name}</span> - {item.muscle_group ?? 'General'}
</li>
))}
</ul>
)}
</div>
);
}

Why is This Code Fragile and Costly to Maintain?​

This file of barely 80 lines already conflates five distinct responsibilities:

  1. HTTP Transport Protocol: Knows the exact URL (http://localhost:3000/api/exercises), HTTP verbs (GET, POST), and request headers (Content-Type).
  2. Raw Backend Format: Directly depends on server naming conventions (exercise_name, muscle_group, difficulty_lvl). If the backend team renames a property to camelCase or refactors the schema, this entire file breaks.
  3. Business Logic and Invariants: Validates that the name contains at least 3 characters and that the muscle group belongs to a whitelist inside a button handler.
  4. Local Reactive State: Coordinates loading, exercises, name, and muscle state variables.
  5. Visual Rendering: Handles JSX elements, semantic tags, and Tailwind CSS utility classes.
The Core Problem: Violation of the Single Responsibility Principle (SRP)

The component has far too many reasons to change. It will change if the REST API contract updates, if business validation rules evolve, if the user interface is redesigned, or if the team decides to migrate from fetch to axios or react-query.


2. Visual Contrast: Monolith vs. Layered Separation​

The diagram below illustrates the structural difference between concentrating all responsibilities into a single file versus distributing them across specialized layers:

Monolithic Component vs. Layered Separation


3. SOLID Principles Applied to the Frontend​

To overcome this anti-pattern in Next.js, we rely on two foundational software design principles:

3.1. Single Responsibility Principle (SRP)​

Each module, class, or function must have a single responsibility and only one reason to change:

  • The View (JSX) should only concern itself with how data is presented to the user.
  • The Use Case should only be responsible for orchestrating the requested user action and executing business rules.
  • The Repository should only handle data persistence and retrieval.
  • The Mapper should only handle transforming data between incompatible formats.

3.2. Dependency Inversion Principle (DIP) and the Dependency Rule​

Clean Architecture creator Robert C. Martin formulates the golden rule:

Source code dependencies must point strictly inward, toward higher-level policies (the Domain).

In the context of Next.js, this means:

  • The Domain does not know about React: No file within the domain layer may import useState, useEffect, next/navigation, or UI dependencies.
  • The UI depends on abstractions: The view consumes use cases and abstract interfaces, rather than concrete HTTP clients or hardcoded URLs.

4. Proposed Project Structure for Next.js​

Adapting Clean Architecture conventions to the Next.js and TypeScript ecosystem, we organize code using Vertical Slices (Features):

Feature-Based Structure in Next.js

Directory Mapping​

src/
├── core/ # Cross-cutting application resources
│ ├── errors/ # Standardized error classes
│ ├── http/ # Configured base HTTP client
│ └── components/ui/ # Generic UI components (Button, Modal, Input)
│
└── features/ # Business modules (Vertical Slices)
└── exercises/ # Exercise feature module
├── domain/ # Pure business rules (TypeScript)
│ ├── entities/ # Immutable entities (exercise.ts)
│ ├── repositories/ # Abstract contracts (exercise.repository.ts)
│ └── usecases/ # Use cases / Interactors (get-exercises.usecase.ts)
│
├── infrastructure/ # External communication adapters
│ ├── dtos/ # Raw API contracts (exercise.dto.ts)
│ ├── mappers/ # DTO <-> Entity converters (exercise.mapper.ts)
│ ├── datasources/ # HTTP connection or in-memory Mock
│ └── repositories/ # Concrete repository implementation
│
├── di/ # Dependency injection / Composition Root
│ └── exercise.container.ts # Dependency assembler
│
└── presentation/ # Visual presentation layer (React / Next.js)
├── hooks/ # Custom Hook Presenter (use-exercises.ts)
└── components/ # Pure visual components (ExerciseCard, ExerciseForm)
Why Organize by Features Instead of Global Folders?

Organizing by global technical directories (src/entities, src/usecases, src/components) scatters related code across the entire project tree. Grouping by feature (features/exercises) preserves high module cohesion and allows developers to isolate changes without affecting other functional areas of the codebase.


5. Lesson Summary​

  1. The spaghetti component violates SRP by mixing networking, business logic, JSON formatting, and JSX into a single file.
  2. Any backend change breaks the view if components directly consume raw API DTOs.
  3. Clean Architecture in Next.js ensures the business core remains pure, immune to framework upgrades or endpoint changes.
  4. In the next lesson, we will explore the Domain Layer in depth, crafting immutable entities and use cases in pure TypeScript.

Self-Assessment Quiz​

Cargando cuestionario...