DEV Community

Cover image for Clean architecture with Next.js
Daniel Malek
Daniel Malek

Posted on • Updated on

Clean architecture with Next.js

1) Introduction and Clean architecture

Software architecture often depends on many things, but there are still some concepts and good practices, that is worth to be familiar with and consider when starting a new project.

The key concepts of Clean Architecture are:
Separation of Concerns: Different parts of the application handle distinct responsibilities, making the code base easier to understand and modify.
Dependency Rule: Inner layers should not depend on outer layers. This improves maintainability, flexibility and reusability, making code modular, easier to refactor and more technology agnostic.
Testability: The architecture facilitates thorough unit testing by isolating components.

In this article I will show how to implement clean architecture using NextJS framework. According to a lot of technology details, I will rather focus on most important parts, so intermediate knowledge about software development would be helpful to understand this article.
I have created a basic Calendar app to show how you can shape a project, check it on github.

 

2) Layers

From technical point of view the idea is based on dividing application into layers and connect them with a bunch of adapters, repository pattern and dependency inversion. On the picture below, you can see how well each concern of any app might be separted. Further, I will describe each part with code examples.

Image description

Source: https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

 

3) Entities and Use Cases

Entities represent core business concepts with their data and rules, forming the heart of the system.
Use Cases define the actions a system can perform, orchestrating how entities interact to achieve specific goals. Think of entities as the building blocks and Use Cases as the blueprints for constructing a software application.

Core business logic can stay in one place, framework independent and well tested. Does it sound good? You can achieve it by utilizing Entities and Use Cases. Database may change, framework may change, but core logic will remain independent in one place. What happen when new business requirements arrive? Just edit proper Use Case.

In the example of the Calendar App I provided, Use Case examples seems way simple (even ICalendarEvent entity is as simple as interface), but their business logic might be extended by adding such features as:

Date and time consistency: Verify that start time precedes end time.
Event duration limits: Define minimum and maximum event durations.
Conflict detection: Check for overlapping events based on event times and locations.
Capacity limits: Enforce attendance restrictions for events.
Category-based filtering: Implement filtering events based on categories.

Remember:

  • Keep Use Cases focused on a single responsibility.
  • Prioritize business logic over technical implementation details (like database structures or UI elements).
  • Write clear and concise code for maintainability.
  • Test your Use Cases to ensure correct behavior.

 

4) Controllers, Presenters and Dependency inversion

Image description

Controller captures an event that takes place in application, invokes a Use Case and, if needed, passes output from Use Case to presenter to map data and make it fit to an UI.

Image description

Controller is a place which connects the outside world with the application's core logic. What is very important to mention here, it also utilizes dependency inversion principle. According to this principle Use Cases should not depend on outer layers. Advantage of dependency inversion principle are:

Improved maintainability: Changes in the underlying implementation (e.g., database) have minimal impact on the Use Cases.
Better code organization: Clear separation of concerns between business logic and technical details.
Enhanced flexibility: You can easily swap out different implementations of dependencies without affecting the core Use Cases.
Increased testability: By isolating Use Cases from external dependencies, you can easily write unit tests without relying on complex setups.

In Calendar App that I have provided, you can see exactly that for example when deleting calendar event, deleteEventUseCase is invoked with repository and output of the Use Case is used to refresh the view. Controllers that I have made receive many arguments, which make them hard to maintain. That might be improved in several ways, for example by using some external JavaScript dependency inversion library or creating your own system for that. A simple improvement in the Calendar Application might be made by creating a hooks controllers, which will encapsulate state and interaction methods and return them:

useCalendarViewController:

export function useCalendarViewController(
  repository: IRepository,
) {
  const [calendarViewData, setCalendarViewData] = useState<TCalendarView | null>(null);

  // const nextCalendarView = async () => {
  // const prevCalendarView = async () => {
  // const fetchCalendarEventData = async (
  // const searchCalendarEvent = async (


  return { calendarViewData, nextCalendarView, prevCalendarView, fetchCalendarEventData, searchCalendarEvent }
}
Enter fullscreen mode Exit fullscreen mode

CalendarViewComponent:

const { calendarViewData, nextCalendarView, prevCalendarView, fetchCalendarEventData, searchCalendarEvent } =  useCalendarViewController(repository);
Enter fullscreen mode Exit fullscreen mode

 

5) Outer layer and Repository pattern

In Clean Architecture, the outer layer is the furthest from the core business logic. It's where all the implementation details reside. This layer is often referred to as the Frameworks and Drivers Layer.

In the Calendar App, you can see that React components are yet another layer, it relays on data and abstraction provided by inner layers. Repository with database implementation details is also passed from here. What is advantage of using repository pattern? At any time you can change database (for example from mongodb to mysql, but also some code details) without touching core business logic, as long as interface fits.

 

6) Summary

We went through the crucial parts of clean architecture. It provides good practices for setting up your project and it is definitely worth considering especially if you know that the project will grow medium or bigger size. For compact projects using all the rules might lead to increased boilerplate code or redundant complexity in the project structure. But there is also a drawback, you need to maintain more code when you pick the approach with repository.

 

7) Bibliography

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
https://www.youtube.com/watch?v=wnxO4AT2N4o
https://betterprogramming.pub/clean-architecture-with-react-cc097a08b105
https://tooploox.com/yet-another-clean-architecture

Top comments (1)

Collapse
 
adam77 profile image
Adam C

What a great article! Everything explained from the basic concepts to details - this should be a standard.