DEV Community

Cover image for Clean Architecture for the Front-End
Nicolas
Nicolas

Posted on

Clean Architecture for the Front-End

🧽 Clean Architecture: Revolutionizing the Front-End

Description: Discover how Clean Architecture can transform and optimize front-end development.

Introduction:

In an increasingly complex computing world, structuring your code effectively is crucial. It relies on the clear separation of an application's components, facilitating testing and reducing dependence on external tools. In this article, we will explore how Clean Architecture can transform your projects by examining its principles, structure, benefits, and its application to a front-end project.


What is Clean Architecture?

Conceived by Robert C. Martin, this software design philosophy places the business concerns of the application at the center of attention. It organizes the code into concentric layers, each with a specific and independent role.

Clean architecture schema
Source : https://miro.medium.com/

Clean Architecture - Divide and Conquer

The primary goal of Clean Architecture is the separation of code into different layers, each with its own responsibility.

Here are the elements that make up this methodical structure:

Entities: Also called the Domain, these are the central elements representing the business logic and rules of the application, independent and can be used by all layers of the application.

Use Cases: They encapsulate all the specific business logic of the application and orchestrate the flow of data between the entities and the outer layers.

Interface Adapters: Also called controllers, presenters, or gateways, they convert data between the formats appropriate for the use cases and entities.

User Interfaces (UIs) and Frameworks & Drivers: They manage interactions with external elements like user interfaces, databases, and external systems.


Adapting to the Front-End

By adapting Clean Architecture to front-end development, we clarify the structure of the code. Entities define data models and business rules, use cases manage user interactions, interface adapters take care of the connection between the UI and the data, and the outer layer interacts with frameworks and external services. This well-defined organization facilitates the maintenance and evolution of front-end applications. Next, let's discover how this approach brings significant benefits to front-end development.

Clean Architecture on the Front-End

The Benefits

The benefits of Clean Architecture for the front-end are similar or identical to those for a back-end application.

  • Framework Independence: Frameworks are then used as tools rather than the other way around.
  • UI Independence: The architecture does not depend on the user interface.
  • Database Independence: It remains unattached to any specific database.
  • Granularity of Tests: It is easy to precisely target a layer/component for testing.

Thanks to this flexibility and increased maintainability, Clean Architecture is a very good choice for large-scale front-end applications.

Implementing this type of architecture indeed incurs a higher cost at the beginning of the project, but the advantages of the method allow for quickly saving time in the future.

Preparation for Implementation

On the back-end side, business rules are fairly straightforward and often intuitive, representing the product's functionalities (creating an account, booking a book, making a payment, etc.). On the front-end side, it's not as evident because the domain (Entities) consists of display rules and the rendering behavior by a browser. Moreover, nothing is defined or conventionalized for the application of Clean Architecture on the Frontend: everything is modular and adaptable according to needs. A small application can very well use only two layers, such as the entities and the outermost layer.

Let's take an example with a task management app. A user has expressed a need, which is translated by our Product Owner into a set of rules that define the steps when the user uses the application.

1. Task Selection: When a user clicks on a task to select it:

  • If the task is already selected, it should be deselected.
  • Otherwise, it is marked as selected.

2. Task Categorization:

  • If a task is categorized as "to do immediately," it moves to the top of the list.
  • If the task is marked "to do later," it is placed in a separate section.

3. Sub-task Management:

  • If a main task is selected, all its associated sub-tasks appear.
  • The user can select or deselect individual sub-tasks.
  • If a main task is deselected, all its sub-tasks are also deselected.

If you're observant, you can already discern a simple description of an algorithm taking shape. That is the domain.


In Practice

Let's revisit a simple task management application.

Here is the structure of our application:

Image description

If we apply the Clean Architecture model, here is what it would look like:

Clean architecture model for Front End

Let's proceed step by step, starting from the center of our architecture by defining our entity Task.ts



interface TaskData {
  id: string;
  title: string;
  description: string;
  completed?: boolean;
}

class Task {
  id: string;
  title: string;
  description: string;
  completed: boolean;

  constructor({ id, title, description, completed = false }: TaskData) {
    this.id = id;
    this.title = title;
    this.description = description;
    this.completed = completed;
  }
}



Enter fullscreen mode Exit fullscreen mode

Next, let's add an adapter that allows us to use our entity. This adapter is very similar to services in a more conventional architecture.



import axios from "axios";
import Task from "../entities/Task";

const TaskService = {
  async fetchTasks() {
    try {
      const response = await axios.get("https://your-api-endpoint.com/tasks");
      return response.data.map((taskData: Task) => new Task(taskData));
    } catch (error) {
      console.error("Error fetching tasks:", error);
      throw error;
    }
  },
  async addTask(taskData) {
    try {
      const response = await axios.post('https://your-api-endpoint.com/tasks', taskData);
      return new Task(response.data);
    } catch (error) {
      console.error('Error adding task:', error);
      throw error;
    }
  },
};

export default TaskService;



Enter fullscreen mode Exit fullscreen mode

Between these two elements, we also need a use case that establishes the link between the entity and the adapter.



// ManageTasks.ts
import Task from '../entities/Task';
import TaskService from '../adapters/TaskService';

class ManageTasks {
  async addTask(taskData: Task): Promise<Task> {
    try {
      return await TaskService.addTask(taskData);
    } catch (error) {
      console.error('Error adding task:', error);
      throw error;
    }
  }

  async fetchAllTasks(): Promise<Task[]> {
    try {
      return await TaskService.fetchAllTasks();
    } catch (error) {
      console.error('Error fetching tasks:', error);
      throw error;
    }
  }
}

export default new ManageTasks();


Enter fullscreen mode Exit fullscreen mode

This allows us, finally, to add this logic into the Task List component, which acts as an adapter facilitating the connection between our internal layers and the Vue framework.



<template>
  <div>
    <ul>
      <li v-for="task in tasks" :key="task.id">
        {{ task.title }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { onMounted, ref } from "vue";
import ManageTasks from "@/use_cases/ManageTasks";
import Task from "@/entities/Task";

onMounted(() => {
  loadTasks();
});

const tasks = ref<Task[]>([]);

async function loadTasks() {
  try {
    tasks.value = await ManageTasks.fetchAllTasks();
  } catch (error) {
    console.error("Failed to load tasks:", error);
  }
}
</script>


Enter fullscreen mode Exit fullscreen mode

Conclusion

The use of Clean Architecture in the front-end, as demonstrated in this example, highlights its numerous advantages.

  1. Framework-Independent Layers:
  • Entities: This core layer of the architecture contains business logic and data models. It is completely independent of Vue.js or any other framework, meaning the code here can be reused regardless of the front-end framework.

  • Use Cases: This layer manages application-specific logic and is also independent of Vue.js. It processes data and interactions without concern for how data is presented or how user inputs are received.

  1. Interface Adapters:

    This layer acts as a bridge between the framework-independent layers and Vue.js-specific layers. Here, we find components that link the external and internal layers. By isolating Vue.js-specific interactions in this layer, the rest of the application remains independent of the framework.

  2. Frameworks & Drivers:

    The outermost layer interacts directly with Vue.js. This includes Vue components and views, directives, filters, and other framework specifics. By concentrating all Vue.js-specific interactions in this outer layer, you reduce your business logic and use cases' dependency on the framework. This makes your application more flexible and easier to test, maintain, or even migrate to another framework if necessary.

By following this structure, Clean Architecture enables the construction of front-end applications where the choice of framework (in this case, Vue.js) does not influence the business logic or the application's rules, offering greater flexibility and improved testability.

🚀So, are you convinced, ready to take the step in your next front-end projects?

Top comments (1)

Collapse
 
snowcoder profile image
Martijn

Issues in your implementation

Why I think it's wrong

It kind of starts with your directory structure. If you have different layers, you should have a directory structure that clearly shows that. Currently, everything is just in /src.

Then you have a Task class, and there is nothing wrong with its definition. But there is something wrong with how you use it. The Task is the only data model you have; it's used in all layers, whereas it should only be used in the domain layer. Yet it's used in your networking and in your presentation. This means Task has more reasons to change; it's become a more fragile component than it should be. Your entities should have one reason to change: a change in the Enterprise Business Logic.

There's nothing much wrong with the TaskService, other than using the domain model.

Then you start at the use case, ManageTasks. Which by no definition is a use case, because use cases are only responsible for doing one thing; your "use case" does two things. Again, this means it has more reasons to change. What you essentially did here is combine two use cases into one. But it doesn't end there; the use case has a dependency on your TaskService. In other words, your use case is dependent on the data layer. This directly violates the Dependency Rule. Dependencies should point to the business rules. Also, once more, this makes your use case more fragile than it should be, because if TaskService changes, your use case needs to change as well.

Then you use the "use case" in your vue file, which is seen more often when mixing it with MVVM, which some frameworks, like Vue, require. It's not too bad, but it means you can't test the view without the business logic. But in your case, the problem cascades further, because your use case also depends on your networking. So you can't test with mock data either.

Why it currently isn't a problem, but it certainly will become one

Currently, this example is so small that this incorrect Clean Architecture implementation isn't really a problem. It's also because the entity contains very simple data, just booleans and strings. It would start to fall apart once you add a single property date, of type Date, to Task:

  • Your networking layer might have issues parsing the date object properly
  • Your presentation would have to format it in a different way

You could probably work around this issue in various ways, but you should be able to see that the networking and presentation layers have different needs, so they need a different data structure.

This entire codebase is also not testable. Which should be a characteristic of a properly implemented Clean Architecture. Say you wanted to test your "use case"; you couldn't because it depends on a concrete type.

How you could fix it

Task stays the same, but place it in:

src/domain/entities/Task

Write two use cases:

src/domain/AddTaskUseCase

src/domain/FetchAllTasksUseCase

Make those use cases depend on an interface within the domain layer called:

src/domain/interfaces/TaskRepository

Implement the TaskRepository in the data layer:

src/net/TaskService

This TaskService then uses a data structure in its layer:

src/net/models/response/TaskResponse

This means you have to map from the TaskResponse object to the Task object.

Then you can use the UseCase in the vue file. But here you should also make a separate display object, like TaskDisplay, that contains data formatted for displays, like a properly formatted date string using the correct locale.

Here is a class diagram of how it would be a lot closer to proper Clean Architecture:

Image description

(please note that your TaskView.vue also depends on AddTaskUseCase, but that would make this diagram less readable)

And even this implementation has some shortcomings. But it mostly aligns with the most important aspects of Clean Architecture.

Having said all that, I still commend you for making this and bringing attention to this topic that I have not really seen a lot of in web development.