<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Roberto Angulo</title>
    <description>The latest articles on DEV Community by Roberto Angulo (@rangulo27).</description>
    <link>https://dev.to/rangulo27</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1886466%2F7a3139a0-0295-4669-bb84-2375349d9466.jpg</url>
      <title>DEV Community: Roberto Angulo</title>
      <link>https://dev.to/rangulo27</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rangulo27"/>
    <language>en</language>
    <item>
      <title>Clean Architecture approach in the frontend, a practical example</title>
      <dc:creator>Roberto Angulo</dc:creator>
      <pubDate>Tue, 21 Oct 2025 07:37:09 +0000</pubDate>
      <link>https://dev.to/rangulo27/clean-architecture-approach-in-the-frontend-a-practical-example-5ih</link>
      <guid>https://dev.to/rangulo27/clean-architecture-approach-in-the-frontend-a-practical-example-5ih</guid>
      <description>&lt;p&gt;One of the most difficult questions to answer as a frontend developer is how to approach architecture within the scope of frontend applications. There’s often a lot of ambiguity around what "architecture" really means in this context.&lt;/p&gt;

&lt;p&gt;Whether we realize it or not, every developer applies some architectural pattern in their projects. Common examples include Flux architecture, Component-Driven architecture, or Micro-frontend architecture. These are powerful and widely used, but they don’t always solve the deeper challenges we face in enterprise-scale applications, where maintaining clear boundaries between business logic and presentation becomes critical—especially in environments with constantly changing requirements.&lt;/p&gt;

&lt;p&gt;Even when our codebase looks clean and modular, with hundreds of components, hooks, and pure functions, we often reach a point where it becomes hard to maintain. We realize there’s no real separation between what the application does (the business logic) and how it does it (the implementation and UI).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem: Business Logic Hidden in Components&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Let’s take a simple example — an e-commerce app with a small React component called ProductPrice that displays the price of a product.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interface Product {
  name: string;
  basePrice: number;
}

export const ProductPrice = ({ product }: { product: Product }) =&amp;gt; {
  // 💥 Business logic embedded inside the UI
  const finalPrice = product.basePrice * 1.1; // Adding 10% tax

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h3&amp;gt;{product.name}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;Final price: ${finalPrice.toFixed(2)}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, this looks fine. But this component is already doing more than it should — it’s calculating prices (business logic) and rendering UI (presentation logic) at the same time.&lt;/p&gt;

&lt;p&gt;Now imagine the business team introduces new requirements:&lt;/p&gt;

&lt;p&gt;Each product can have its own tax rate and discount percentage based on category or promotions. This forces us to modify the component to handle new rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const ProductPrice = ({ product }: { product: Product }) =&amp;gt; {
  // 💥 Logic grows and mixes with presentation
  const finalPrice =
    product.basePrice *
    (1 - (product.discountPercentage ?? 0)) *
    (1 + (product.taxRate ?? 0));

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h3&amp;gt;{product.name}&amp;lt;/h3&amp;gt;
      &amp;lt;p&amp;gt;Final price: ${finalPrice.toFixed(2)}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, not catastrophic—but imagine scaling this pattern across dozens of components. Soon you’ll have duplicated logic, tight coupling, and a fragile codebase where every change in the business rules requires modifying multiple UI components.&lt;/p&gt;

&lt;p&gt;This violates Separation of Concerns and makes it hard to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Test business logic independently from the UI.&lt;/li&gt;
&lt;li&gt;Reuse logic across different parts of the app.&lt;/li&gt;
&lt;li&gt;Adapt to new requirements without breaking existing behavior.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Solution: Clean Architecture
&lt;/h2&gt;

&lt;p&gt;To solve this, we can apply Clean Architecture, a concept popularized by Uncle Bob (Robert C. Martin). Its goal is to build software that is modular, maintainable, and independent of frameworks or external details.&lt;/p&gt;

&lt;p&gt;In Clean Architecture, the codebase is divided into layers, each with a single responsibility:&lt;/p&gt;

&lt;p&gt;Entities → Use Cases → Interface Adapters → Frameworks/UI&lt;/p&gt;

&lt;p&gt;Each layer depends inward, toward the domain, never outward.&lt;br&gt;
This keeps your business logic isolated from technical details like React, APIs, or databases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Entities (Domain Models)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Entities represent the core business concepts and rules of your system. They’re pure, framework-agnostic objects — they don’t depend on React, APIs, or external libraries.&lt;/p&gt;

&lt;p&gt;For our example, let’s define the Product entity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// domain/entities/Product.ts
export interface Product {
  id: string;
  name: string;
  basePrice: number;
  discountPercentage?: number;
  taxRate?: number;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This entity describes what a product is, not how it’s used or displayed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use Cases (Application Logic)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Use cases represent what the application does — they orchestrate business logic by coordinating entities.&lt;br&gt;
They’re the only layer allowed to manipulate entities directly.&lt;/p&gt;

&lt;p&gt;For example, let’s define a use case that calculates the final price of a product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// domain/usecases/CalculateProductPrice.ts
import { Product } from "../entities/Product";

export class CalculateProductPrice {
  execute(product: Product): number {
    const discount = product.discountPercentage ?? 0;
    const tax = product.taxRate ?? 0;

    return product.basePrice * (1 - discount) * (1 + tax);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And another use case to retrieve a product by ID (which will depend on an abstraction of a repository):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// domain/usecases/GetProductById.ts
import { Product } from "../entities/Product";
import { ProductRepository } from "../../infrastructure/repositories/ProductRepository";

export class GetProductById {
  constructor(private readonly productRepo: ProductRepository) {}

  async execute(id: string): Promise&amp;lt;Product | null&amp;gt; {
    return await this.productRepo.getProductById(id);
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that use cases depend only on abstractions (interfaces), never on concrete implementations. This aligns with the Dependency Inversion Principle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Infrastructure (Adapters &amp;amp; Repositories)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The Infrastructure layer implements the low-level details: APIs, databases, HTTP clients, etc.&lt;br&gt;
It provides concrete implementations of the abstractions defined in the domain.&lt;/p&gt;

&lt;p&gt;Here’s an example using Supabase as our data source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// infrastructure/repositories/ProductRepository.ts
import { Product } from "../../domain/entities/Product";

export interface ProductRepository {
  getProductById(id: string): Promise&amp;lt;Product | null&amp;gt;;
}

// infrastructure/repositories/SupabaseProductAdapter.ts
import { SupabaseClient } from "@supabase/supabase-js";
import { Product } from "../../domain/entities/Product";
import { ProductRepository } from "./ProductRepository";

export class SupabaseProductAdapter implements ProductRepository {
  constructor(private readonly client: SupabaseClient) {}

  async getProductById(id: string): Promise&amp;lt;Product | null&amp;gt; {
    const { data, error } = await this.client
      .from("products")
      .select("*")
      .eq("id", id)
      .single();

    if (error) throw error;
    return data as Product;
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the initialization of the Supabase client itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// infrastructure/supabase/initClient.ts
import { createClient } from "@supabase/supabase-js";

export const initSupabaseClient = (url: string, anonKey: string) =&amp;gt;
  createClient(url, anonKey);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Frameworks / UI Layer (React)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Finally, at the outermost layer, we have the Frameworks/UI — in this case, React.&lt;br&gt;
This layer should be responsible only for presenting data and handling user interactions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// ui/components/ProductCard.tsx
import React, { useEffect, useState } from "react";
import { initSupabaseClient } from "@/infrastructure/supabase/initClient";
import { SupabaseProductAdapter } from "@/infrastructure/repositories/SupabaseProductAdapter";
import { GetProductById } from "@/domain/usecases/GetProductById";
import { CalculateProductPrice } from "@/domain/usecases/CalculateProductPrice";
import { Product } from "@/domain/entities/Product";

const supabaseClient = initSupabaseClient(
  import.meta.env.VITE_SUPABASE_URL!,
  import.meta.env.VITE_SUPABASE_KEY!
);

const productRepository = new SupabaseProductAdapter(supabaseClient);
const getProductByIdUseCase = new GetProductById(productRepository);
const calculateProductPriceUseCase = new CalculateProductPrice();

interface ProductCardProps {
  productId: string;
}

export const ProductCard = ({ productId }: ProductCardProps) =&amp;gt; {
  const [product, setProduct] = useState&amp;lt;Product | null&amp;gt;(null);

  useEffect(() =&amp;gt; {
    getProductByIdUseCase.execute(productId).then(setProduct).catch(console.error);
  }, [productId]);

  if (!product) return &amp;lt;div&amp;gt;Loading...&amp;lt;/div&amp;gt;;

  const finalPrice = calculateProductPriceUseCase.execute(product);

  return (
    &amp;lt;div className="product-card"&amp;gt;
      &amp;lt;h2&amp;gt;{product.name}&amp;lt;/h2&amp;gt;
      &amp;lt;p&amp;gt;Final price: ${finalPrice.toFixed(2)}&amp;lt;/p&amp;gt;
    &amp;lt;/div&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, the UI only renders and calls use cases.&lt;br&gt;
If the business rules for pricing change tomorrow, we’ll update only CalculateProductPrice.ts, without touching any React component.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Let’s summarize how each layer works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Entities: Core business models and rules =&amp;gt; Product
= Use Cases: Define application behavior (what the app does) =&amp;gt; CalculateProductPrice, GetProductById&lt;/li&gt;
&lt;li&gt;Adapters / Infrastructure: Concrete implementations (how data is retrieved or stored) =&amp;gt; SupabaseProductAdapter&lt;/li&gt;
&lt;li&gt;Frameworks / UI =&amp;gt; Presentation layer using frameworks like React ProductCard component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While it may seem over-engineered for small projects, Clean Architecture shines in enterprise-grade applications or long-lived projects, especially when business rules evolve frequently.&lt;/p&gt;

&lt;p&gt;By enforcing these boundaries, you build software that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Modular — each part has a clear responsibility.&lt;/li&gt;
&lt;li&gt;Testable — business logic can be tested independently from UI.&lt;/li&gt;
&lt;li&gt;Maintainable — changes in requirements don’t ripple through unrelated code.&lt;/li&gt;
&lt;li&gt;Framework-agnostic — your core logic survives even if you switch from React to another framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ultimately, Clean Architecture allows frontend developers to think beyond components — and start designing software that truly scales with complexity.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
