Implementing onion architecture using Rust

Implementing onion architecture using Rust

Today, I will implement onion architecture using rust, we will make a simple with Actix, which is a compelling web framework. (https://actix.rs/) You will find here how I have implemented it: https://github.com/mathias-vandaele/receipts_inserter_onion First, let's try to understand what is the onion architecture

The onion architecture

Onion architecture is a way to architecture your project in a layered manner.

This technic aims to create better separation of concerns, testability, maintainability, etc. It respects every SOLID principle. Here is a corresponding image of how onion architecture is layered.

Clean Architecture Flashcards | Quizlet

The way I made it is a little bit different, but I find it more straightforward and more understandable

  • Domain:

It's in charge of the Data Access Object (DAO), which is a fancy way of saying it handles how we talk to our data. The domain layer also has this thing called a 'repository trait.' Think of it like a blueprint for how we'll interact with data from the outside. We'll get into the nitty-gritty in a bit, but just know that the repository trait will be brought to life by the infra layer. It's like a set of rules the infra layer will follow to play nice with the data.

#[async_trait]
pub trait ReceiptRepository{
    async fn find_by_id(&self, id: String) -> Result<Receipt, Box<dyn Error>>;
    async fn insert(&self, receipt : Receipt) -> Result<(), Box<dyn Error>>;
}
  • Infra:

The infra layer is like the backstage crew for our code. It's responsible for making sure all the tools the domain layer needs are ready to go. So, when the use cases (we'll get to those in a bit) need something from the domain, the infra layer is there to provide it from whereever (database, API etc.). Think of it as a helpful assistant that hands over everything the main show (our app) needs to run smoothly.

    pub fn provide_mongo_collection(&self, collection : String) -> impl ReceiptRepository + Send + Sync {
        MongoCollection::new(self.mongo_client.collection::<Receipt>(&collection))
    }
  • Use Cases

Alright, let's talk about the use case. This is where the actual brainpower is. Imagine it as the decision-maker of our app. It's got this 'execute' function that does the heavy lifting. This function knows all the moves to get data in and out of the database. How does it know? With the repositories we talked about earlier. They give it the secret codes it needs to work its magic. Think of the use case as the superhero of our app, making sure everything happens just the way it should.

    pub async fn execute(&self, id: String) -> Result<domain::Receipt, Box<dyn std::error::Error>> {
        self.collection.find_by_id(id).await
    }
  • Dependency Injection:

This module holds all the essential stuff the app needs, like connections to the database or other APIs. It's like a treasure chest of tools. But It's also a matchmaker. When we need a specific job done (that's where the use cases come in), it finds the perfect worker from the infra team, hooks them up, and creates a dream team for that task. This super-smooth teamwork means our app's different parts don't get tangled up; this is what low coupling is. It's like having a well-organized team that can pass the ball flawlessly.

This is FeatureContainer

    pub fn get_receipt(&self, from : String) -> GetReceipt {
        GetReceipt::new(Box::new(self.infra_provider.provide_mongo_collection(from)))
    }
  • Presentation :

The presentation layer is our app's front door for customers. It's like the face of our project, the part you'll start up. This is where the magic begins! It's the one that fires up the Actix web server and sets the rules for how it talks to the outside world.

The error manager is like the translator that takes the messy technical stuff and turns it into plain English for the customer.

The presentation layer also keeps 'DTO' – that's short for Data Transfer Object. It's like the messenger that carries data back and forth with the client; it turns this data into the proper format that the layer underneath understands (DAO), like translating between two languages. This layer also holds an instance of FeatureContainer, with essential connections. This container's job is to ensure everyone has what they need, like a backstage organizer (what we talked about just above).

#[get("/receipt/{id}")]
pub async fn get_receipt(
    data: Data<FeatureContainer>,
    req: HttpRequest,
    id: web::Path<String>,
) -> Result<HttpResponse, ApiError> {

    let from = req.headers().get("from").ok_or_else(|| {
        ApiError::new(
            "Please pass a correct header".to_string(),
            "There is no header from".to_string(),
            actix_web::http::StatusCode::BAD_REQUEST,
        )
    })?.to_str().map_err(|err| {
        ApiError::new(
            "Please pass a correct header".to_string(),
            err.to_string(),
            actix_web::http::StatusCode::BAD_REQUEST,
        )
    })?.to_string();

    match data.get_receipt(from).execute(id.into_inner()).await {
        Ok(receipt) => Ok(HttpResponse::Ok().json(ReceiptDto::from(receipt))),
        Err(err) => Err(ApiError::new(
            "Error inserting receipt".to_string(),
            err.to_string(),
            actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
        )),
    }
}

Conclusion

If you want to see the code more in-depth, don't hesitate to take a look at: https://github.com/mathias-vandaele/receipts_inserter_onion