Practical Clean Architecture in Typescript, Rust & Python
Tags: typescript, rust, python, cleanarchitecture
Published: 2022-05-17 21:14:35

## Goal

The goal of this article is to share starter templates in Typescript, Rust & Python for the Rest API, using Clean Architecture.

## TL;DR

- Typescript: https://github.com/MSC29/clean-architecture-typescript
- Rust: https://github.com/MSC29/clean-architecture-rust
- Python: https://github.com/MSC29/clean-architecture-python
- for tech details jump at the bottom to the `Implementation`

## Ok but why?

I've been using this Clean Architecture in several projects recently, mostly in Typescript & Rust and recently tried with Python too.

I'd like to encourage software engineers to use clean code & clean architecture patterns because, among other things, it helps preventing tight coupling between infrastructure & business logic and writing code that's easier to maintain.

I've seen enough projects in my professional experience with a lack of well defined structure, lots of tight coupling between business logic & database's DAO objects, low or lack of test coverage, very hard maintenance capabilities, high complexity preventing the on-boarding of a developer on the project, etc. That tells me that there's still in 2022 a real lack of good code quality in many companies. If "it works", it's good enough right?

## What does this article contain?

The exercise here is to build this Rest API, declined in Typescript, Rust & Python, with the same functional requirements and with Clean Architecture.
The tech stacks vary and the whole point of Clean Architecture is that we should be able to isolate the Business logic from the Infrastructure and change one without affecting the other.

Disclaimer; please keep in mind this is a template which is not production ready as it lacks security features, advanced testing, authentication, CI/CD, ...
However, I'm looking forward to any suggestions you might have :)

## Clean architecture

The goal isn't to explain Clean Architecture, there are so many great articles out there, check out these very good tutorials:

- [blog.cleancoder.com](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
- [quick-introduction-to-clean-architecture](https://www-freecodecamp-org.cdn.ampproject.org/v/s/www.freecodecamp.org/news/a-quick-introduction-to-clean-architecture-990c014448d2/amp/?amp_js_v=a6&_gsa=1&usqp=mq331AQFKAGwASA%3D#aoh=16054829622148&referrer=https%3A%2F%2Fwww.google.com&_tf=From%20%251%24s&ampshare=https%3A%2F%2Fwww.freecodecamp.org%2Fnews%2Fa-quick-introduction-to-clean-architecture-990c014448d2%2F)
- [clean-architecture-introduction](https://pusher.com/tutorials/clean-architecture-introduction/)
- [brief intro to clean architecture](https://medium.com/software-alchemy/a-brief-intro-to-clean-architecture-clean-ddd-and-cqrs-23243c3f31b3)
- [hexagonal-architecture](https://beyondxscratch.com/2017/08/19/hexagonal-architecture-the-practical-guide-for-a-clean-architecture/)
- [the dependency rule](https://www.informit.com/articles/article.aspx?p=2832399)

## API functional requirements

We're going to build a project for a hypothetical client who loves animals, especially dogs & cats and wants to expose an Http Rest API that provides Dogs & Cats Facts. Why not, right?

We need to use 2 sources:

- cat facts: `https://catfact.ninja`
- dog facts: knowledge acquired over the ages that should be stored in a `database`

(see where this going :D ?)

We have technical constraint on the API data output:

- Cat facts should be formatted as such

  ```json
  {
    fact: String,
    nb_chars: Integer
  }
  ```

- Dog facts should be formatted as such

  ```json
  {
    fact_id: Integer,
    txt: String
  }
  ```

The client needs to fetch all cat facts and all dog facts, but also individual dog facts and random cat facts.

That's pretty much it.

Oh... and the client isn't sure about the technology of the database, may run out of budget and change it later and will probably use another source for cat facts in the future. Alice knows Rust, Bob is keen on Typescript and Mallory loves Python, they're undecided about the language to use so we're going to build the same API 3 times, obviously, right? Here's the proposal:

- Typescript: Fastify, axios & TypeOrm (PostgreSQL)
- Rust: ActixWeb, reqwest & Diesel Orm (PostgreSQL)
- Python: Fast API, requests & Peewee Orm (SQLite)

No need to worry about security, their infrastructure is so very secure, this is 2022 c'mon.

## Design

### Overview

The Clean Architecture pattern seems like a good idea since the business requirements are clear (that never happens) and the infrastructure which is likely to change (that happens a lot and we caught it in time).

![clean architecture](https://blog.cleancoder.com/uncle-bob/images/2012-08-13-the-clean-architecture/CleanArchitecture.jpg)

_Source and credit: [https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)_

### Logical entities (Domain layer)

The protagonists are Cat & Dog Facts, but we should also have some configuration, a generic error, ...

> These represent our most abstracted logic level that we want to manipulate throughout the entire code base. In Clean Architecture, the Dependency Rule tells us that Domain Layer objects are accessible in every Layer (black arrow below).

![clean architecture cone](https://www.codingblocks.net/wp-content/uploads/2018/02/The-Clean-Architecture-Cone.jpg)

_Source and credit: [https://www.codingblocks.net/podcast/clean-architecture-make-your-architecture-scream/](https://www.codingblocks.net/podcast/clean-architecture-make-your-architecture-scream/)_

These are Plain Old T(ypescript)/R(ust)/P(ython) Objects.

```
CatFactEntity:
  fact_txt: String
  fact_length: Integer
```

```
DogFactEntity:
  fact_id: Integer
  fact: String
```

### Business Use Cases (Application layer)

Business Use Cases are **NOT AFFECTED** by the infrastructure choices. An algorithm that `SUMS variable1 to variable2` should return the sum, regardless of where the data is coming from (PostgreSQL, Hadoop, an Excel Spreadsheet, a Wormhole, ...), right?!

> Therefore there should **NEVER** be an import of the adapter/infrastructure layer in either the Application or Domain layer. That's where the Dependency Rule makes sense. The lower layers should not know about the higher layers (Application & Domain, the logic, can't know about the technologies choices). The opposite isn't true however, the external layer knows about the internal layer(s).

![infra vs domain](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qz6yyv4y1bht1fvkgvpu.png)

_Source and credit: [https://pusher.com/tutorials/clean-architecture-introduction/](https://pusher.com/tutorials/clean-architecture-introduction/)_

Since this is a Rest API, I try to associate 1 Use Case with 1 route. Obviously, in a complex application, some very generic Use Cases could be reused. Internet is full of debates on the subject, since a Use Case is tied to an action the application is doing, there should be 1 Use Case per route only... but Clean Code also tells us not to reuse code... enjoy the developer debates :)

As stated above in the functional requirements

- `GetAllCatFactsUseCase(repository_interface)`: fetch all cat facts
- `GetOneRandomCatFactUseCase(repository_interface)`: fetch one random cat fact
- `GetAllDogFactsUseCase(repository_interface)`: fetch all dog facts
- `GetOneDogFactByIdUseCase(dog_fact_id: Integer, repository_interface)`: fetch an individual dog fact

> These Use Cases accept as parameters the `repository_interface`. That way, we don't have to know what technology is going to be used by the actual repository (cat_fact_repository_mysql or cat_fact_repository_hadoop, cat_fact_repository_http, ...). We only access the "abstract" methods defined by the interface. That's one of the mechanisms of [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection).

### Interfaces (Adapter layer)

Clean architecture is derived from Hexagonal Architecture, hence the terms `API` (Application Programming Interface: client facing) & `SPI` (Service Provider Interface: the opposite side of clients, pointing to data/service providers).

![hexagonal architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ge9vh1wb2a3hiu112u9n.png)

_Source and credit: [https://beyondxscratch.com/2017/08/19/hexagonal-architecture-the-practical-guide-for-a-clean-architecture/](https://beyondxscratch.com/2017/08/19/hexagonal-architecture-the-practical-guide-for-a-clean-architecture/)_

Here, both the API & SPI are related to **bold** technological choices; tools from the Web Server or Framework, an Http library, a SQL driver, an ORM, ...

#### SPI

The data sources are in the functional requirements above.

> Repositories are responsible for fetching the data. The connection object to the data source is injected (an Http library, a sql driver manager, ...) and used to access the data. The reason why this works regardless of the technology chosen is because we use Interfaces defined in the Application Layer.

`DogFactsRepositoryAbstract` defines `get_dog_fact_by_id(dog_fact_id: Integer)` and `get_all_dog_facts`. Regardless of the technology chosen to access the data source, these methods **WILL BE ACCESSIBLE** to the Use Cases.

The Cat API `https://catfact.ninja` returns cat facts in the format:

```json
{
  "fact": "cat fact",
  "length": 8
}
```

The DB (for obscure reasons) data model is

```sql
CREATE TABLE dog_facts (id INTEGER PRIMARY KEY AUTOINCREMENT, fact TEXT)
```

Not quite the data formats we want to output in our API.

No problem. For that we use Mappers that are classes responsible for translating an SPI object into its Domain Layer equivalent. For example, a CatFactApiModel object coming back from the external Cat Fact API into a Domain CatFactEntity. That way, the repository (Adapter Layer) returns to the Use Case (Application Layer, below) an Entity (Domain Layer); Dependency Rule respected.

#### API

Simple REST:

- `GET /api/v1/cats/`: all cat facts
- `GET /api/v1/cats/random`: a random cat facts
- `GET /api/v1/dogs/`: all dog facts
- `GET /api/v1/dogs/{id}`: a dog fact with id {id}

> These are defined or handled by the Controllers. Their responsibility is to control the data coming in & out of the API. Anything related to logic should be done in a Use Case. This is where the Infrastructure level configuration or repository implementation has to be injected into the Use Case.

### Environments (Infrastructure layer)

Based on the environment variable ENV, it'll get the app setting from the `.env.<ENV>` file (`.env.dev`, `.env.test`, ...).

> This is where all the overall server configuration is found; security plugins, data validations, ports configurations, ... This is as related to the infrastructure technology choice as it gets.

### Overview

So that gives us this example of data flow through all the layers of the Clean Architecture.

![Life Cycle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t180jxvrslkaz996vn3j.png)
 

## Testing

Doing a few simple tests always goes a long way. This isn't a test related article so I won't get into details here. All 3 implementations have the same tests.

### Unit tests

The focus here is on the Use Cases, because they represent the business Use Cases that NEVER change, even if the infrastructure changes.
Obviously, other classes/files could have been unit tested, such as mappers, repositories but this is not the point of this exercise:

- tests for GetAllCatFactsUseCase:

  - should raise exception when unexpected repo exception
  - should raise exception when expected repo exception
  - should return empty list
  - should return list

- tests for GetAllDogFactsUseCase:

  - should raise exception when unexpected repo exception
  - should raise exception when expected repo exception
  - should return empty list
  - should return list

- tests for GetOneDogFactByIdUseCase:

  - should raise exception when unexpected repo exception
  - should raise exception when expected repo exception
  - should return one result

- tests for GetOneRandomCatFactUseCase:
  - should raise exception when unexpected repo exception
  - should raise exception when expected repo exception
  - should return one result

### Integration tests

The focus here is on the routes and their valid usage, because they represent the data that comes back in the Presenter format, regardless of the infrastructure or SPIs (3rd party API, database, ...). Obviously, other cases, such as exceptions, security, ... could have been tested but this is not the point of this exercise:

- file: test_cat_facts
  - should return multiple results for /api/v1/cats/facts
  - should return one results only for /api/v1/cats/random
- file: test_dog_facts
  - should return multiple results for /api/v1/dogs/facts
  - should return one results only for /api/v1/dogs/{id}

## Implementation

Here, I'm listing the characteristics of each declination of the API into each language.

### Characteristics

#### Typescript

github repo: [https://github.com/MSC29/clean-architecture-typescript](https://github.com/MSC29/clean-architecture-typescript)

Web Server (Infrastructure layer):

- Fastify
- http://localhost:8080

SPI (Adapter layer):

- Http lib: axios
- Db: TypeORM + PostgreSQL

#### Rust

github repo: [https://github.com/MSC29/clean-architecture-rust](https://github.com/MSC29/clean-architecture-rust)

Web Server (Infrastructure layer):

- ActixWeb
- http://localhost:8888

SPI (Adapter layer):

- Http lib: reqwest
- Db: Diesel Orm + PostgreSQL

#### Python

github repo: [https://github.com/MSC29/clean-architecture-python](https://github.com/MSC29/clean-architecture-python)

Web Server (Infrastructure layer):

- FastAPI
- http://localhost:8000

SPI (Adapter layer):

- Http lib: requests
- Db: Peewee Orm + SQLite

### Similarities & differences between all 3 projects

Pretty much everything is the same between all 3 projects (the folder structure, routes names & schema, tests, variable names, error texts, ...) to make it easy to compare & understand. There are obvious differences when it comes to the case, imports, tests names, ... which are down to the language & frameworks used.

Some other differences exist, where I wanted to try something differently such as the helper methods for tests, the instantiation of the app or the mock api server for the Http SPI. But that does not change the fact the tests are the same and the API interfaces are the same and business Use Cases are the same :)

### Changing the Infrastructure

The SPI (Adapter layer) are "easily" changeable because they're not related to the Server/Framework used nor to the Business Use Cases in the Application & Domain layers. In the repositories, there's no import of the lib used to query the Database or Http endpoints. This, however, could be further improved by implementing all methods we'd need to use inside the `DbConnection` and `HttpConnection` (get, post, find, etc.).

Changing Http lib or database should only affect the SPI repository and the Connection injected. In a more complex project, you may have very specific SQL (for Oracle for example), which might not port well to another Database (SqlServer for example). There will be changes to make, but at least your business Use Cases won't budge.

In my implementation, I'm not completely separating the Infrastructure from the Adapter layer when the Web Server is concerned. The routes definitions and some injectors from the Adapter layer are directly related to the Server used... That being said, the move to another framework affects only 2 layers in very well defined places and would not impact the SPI. If you were to switch to Flask instead of FastAPI in Python, the Infrastructure & API controllers/routes would change a bit, but not the Http or Db repositories, nor the Use Cases.

### Other benefits of the Clean Architecture

On top of what's stated above, this framework helps a lot with the overall structure of the code. It's very obvious to determine where code should go: pieces of Business logic in the Use Cases, security configuration in the Infrastructure's configuration, pagination of external Apis in the Adapter SPI repositories, etc.

Unit testing Use Cases becomes obvious & simple. Simply inject the repository(ies) needed as Stubs to mock their behaviors and get a good coverage & very detailed understanding of your Business logic. That's a huge benefit in complex applications. You could even work hand to hand with your Product Owner and provide them with your BDD test execution list as Acceptance Criteria, but I'm getting sidetracked...

## Conclusion

I hope this helps starting off with a Clean Architecture project; whether it were in Typescript, Rust or Python.

I've got more examples and things that I've done again differently in other projects. This is a very simple project where the Use Cases are pretty slim and there aren't POST/PUT/DELETE requests. But nothing changes, the same logic applies.

Remember, the whole point is to separate the code in an intelligible way and make maintenance & testing easy.

In a company you'll be asked to maintain an application, make changes & never create downtime for the clients. You'll also be asked to change a framework or a database for budget reasons and onboard new joiners to the project.
In my own experience, doing all of the above has been **a lot** simpler with Clean Architecture, despite the initial learning curve.

I hope you enjoyed that, I certainly did, writing the same API in 3 different languages has been fun! :)

I'd love to hear suggestions, feel free to get in touch.