Logo

dev-resources.site

for different kinds of informations.

One of many ways to migrate from NodeJS to Rust

Published at
11/22/2023
Categories
rust
backend
microservices
cqrs
Author
ddkand
Categories
4 categories in total
rust
open
backend
open
microservices
open
cqrs
open
Author
6 person written this
ddkand
open
One of many ways to migrate from NodeJS to Rust

This post describes my personal approach and the experience I have gained. It may contain some deviations, but they are not critical to understanding and usage.

My goal is to develop a microservice in Rust that matches the speed, safety, and ease of development found in NodeJS and Typescript with NestJS.
Whats more important to me is the utilization of modern system design patterns such as IoC, DDD, CQRS, and Hexagonal architecture, without relying on complex frameworks.

To start with, as I mentioned earlier, this post wont involve complex frameworks such as rocket_rs or actix_web, among others of the sort. This choice is deliberate because delving into such frameworks tends to require learning numerous components that may not significantly contribute to creating clear and comprehensible software. However, if you prefer to use frameworks, its entirely fine — just not the approach I am taking here.

Lets begin preparing to create the Todo microservice in Rust. Initially, we need to decide on the crates that will be utilized in the microservice. For this purpose, I published my own implementations of IoC & CQRS. I chose this route to gain a deeper understanding of these methodologies, as some other crates were either outdated or lacked support from the community.

Links of crates:
IoC container
CQRS implementation
CQRS implementation with IoC wrapper

Other crates of microservice:

  • tonic - grpc implementation
  • hyper - http implementation
  • sqlx - driver for managing PostgreSQL
  • tokio - handling asynchronous operations

Thats all we will be using.

I lean toward an approach where the application is divided into smaller slices, encapsulated within different packages for reuse. The cargo workspaces package structure will look like this.

  1. API - main app to client access calls
  2. Common - domain area with interfaces for core packages
  3. Config - contain any environment vars to provide it in packages
  4. Core - business logic, such as CRUD operations and etc
  5. Repository - persisted store layer
  6. Schema GRPC - proto files and network contracts to access MS

Lets take a closer look each of them.

API package contain grpc server to launch ours grpc controllers, by current example its will be only one controller with based CRUD operations,
http server with health checks controller its will be used to any probes of kubernetes.

Common package will contains only domain entities - actually its not so good place for them, but now its ok.
And entirely interfaces to implementation of database repositories, business services and etc.

Config package its pretty simple lib which provide store of environment variables. Like database credentials, app hosts and ports.

Repository its more difficult layer which will working only with database, in current case its will be postgres.

Schema GRPC package to accumulate proto files and generating bin files for communication with MS via API

Diagram:
Diagram of modules

The previous section was more theoretical and now we will dive to code.

We will start by creating a simple todo controller responsible for handling client requests.

pub struct TodoGrpcController {
  context: ContainerContext,
}

impl TodoGrpcController {
  pub fn new(props: ContainerContextProps) -> Self {
    Self {
      context: ContainerContext::new(props),
    }
  }

  fn get_bus(&self) -> Box<CqrsProvider::Provider<AppContext>> {
    self.context.resolve_provider(CqrsProvider::TOKEN_PROVIDER)
  }
}

#[async_trait]
impl TodoService for TodoGrpcController {
  async fn create(
    &self,
    request: Request<CreateTodoRequest>,
  ) -> Result<Response<CreateTodoResponse>, Status> {
    let request = request.into_inner();

    let bus = self.get_bus();

    let command = CreateTodoCase::Command::new(&request.name, &request.description);

    let todo_entity = bus.command(Box::new(command)).await.unwrap();

    Ok(Response::new(CreateTodoResponse {
      status_code: 200,
      message: String::from("CREATED"),
      data: Some(Todo {
        id: todo_entity.get_id().to_string(),
        name: todo_entity.get_name().to_string(),
        description: todo_entity.get_description().to_string(),
        completed: todo_entity.get_completed(),
        created_at: todo_entity.get_created_at().to_string(),
        updated_at: todo_entity.get_updated_at().to_string(),
      }),
    }))
  }

  async fn get_by_id(
    &self,
    request: Request<GetTodoByIdRequest>,
  ) -> Result<Response<GetTodoByIdResponse>, Status> {
    let request = request.into_inner();

    let bus = self.get_bus();

    let query = GetTodoByIdCase::Query::new(&request.id);

    match bus.query(Box::new(query)).await.unwrap() {
      Some(r) => Ok(Response::new(GetTodoByIdResponse {
        status_code: 200,
        message: String::from("SUCCESS"),
        data: Some(Todo {
          id: r.get_id().to_string(),
          name: r.get_name().to_string(),
          description: r.get_description().to_string(),
          completed: r.get_completed(),
          created_at: r.get_created_at().to_string(),
          updated_at: r.get_updated_at().to_string(),
        }),
      })),
      None => return Err(Status::not_found("Not found todo by id.")),
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The methods are pretty concise, each containing fundamental logic. For instance, lets delve into the creation of a todo item. Initially, I have introduced a method to retrieve the CQRS bus (get_bus), which serves as a pathway for routing commands or queries.

Subsequently, a command is crafted and sent to the command bus using bus.command. Following this, the tasks logic is delegated to the command handler. These handlers operate as layers responsible for executing various business cases, such as creating new todo items.

Lets explore the code for the creation of a todo in the command handler:

#[derive(Clone)]
  pub struct Command {
    name: String,
    description: String,
  }

  impl Command {
    pub fn new(name: &str, description: &str) -> Self {
      Self {
        name: name.to_string(),
        description: description.to_string(),
      }
    }
  }

  #[async_trait]
  impl CommandHandler for Command {
    type Context = AppContext;
    type Output = Result<TodoEntity, Box<dyn Error>>;

    async fn execute(&self, context: Arc<Mutex<Self::Context>>) -> Self::Output {
      let repository = context.lock().unwrap().get_command().get_repository();

      repository
        .create(&TodoEntity::new(
          &Uuid::new_v4().to_string(),
          &self.name,
          &self.description,
          false,
          &Local::now().to_string(),
          &Local::now().to_string(),
        ))
        .await
    }
  }
Enter fullscreen mode Exit fullscreen mode

The logic here is rather straightforward. It involves the creation of a new todo entity and handling the persistence by repository with the PostgreSQL database through sqlx.

In the repository layers, its important to follow a key principle from CQRS, which suggests splitting read and write actions. This means having two separate database pools — one for writing data and the other for reading it. Yet, for simplicity in this example, I have chosen to create two simplified versions: one for executing commands and another for dealing with queries. You can adjust this setup to match your own needs.

The todo query repository code:

#[derive(Clone)]
pub struct SqlxQueryRepository {
  pool: PgPool,
}

impl SqlxQueryRepository {
  pub fn new(pool: PgPool) -> Self {
    Self { pool }
  }
}

#[async_trait]
impl TodoQueryRepository for SqlxQueryRepository {
  async fn get_by_id(&self, id: &str) -> Result<Option<TodoEntity>, Box<dyn Error>> {
    let row = sqlx::query(
      "
      SELECT * FROM todo WHERE id = $1 LIMIT 1
    ",
    )
    .bind(Uuid::parse_str(id).unwrap())
    .fetch_one(&self.pool)
    .await;

    match row {
      Ok(r) => Ok(Some(TodoSqlxMapper::pg_row_to_entity(&r))),
      Err(_) => return Err("Cant find todo by id".into()),
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

And todo command repository code:

#[derive(Clone)]
pub struct SqlxCommandRepository {
  pool: PgPool,
}

impl SqlxCommandRepository {
  pub fn new(pool: PgPool) -> Self {
    Self { pool }
  }
}

#[async_trait]
impl TodoCommandRepository for SqlxCommandRepository {
  async fn create(&self, todo: &TodoEntity) -> Result<TodoEntity, Box<dyn Error>> {
    let created_at = match sqlx_parse_utils::string_to_timestamp(&todo.get_created_at()) {
      Ok(r) => r,
      Err(e) => return Err(e.into()),
    };

    let updated_at = match sqlx_parse_utils::string_to_timestamp(&todo.get_updated_at()) {
      Ok(r) => r,
      Err(e) => return Err(e.into()),
    };

    let row = sqlx::query(
      "
      INSERT INTO todo (id, name, description, completed, created_at, updated_at)
      VALUES ($1, $2, $3, $4, $5, $6) RETURNING *
    ",
    )
    .bind(Uuid::parse_str(&todo.get_id()).unwrap())
    .bind(&todo.get_name())
    .bind(&todo.get_description())
    .bind(&todo.get_completed())
    .bind(created_at)
    .bind(updated_at)
    .fetch_one(&self.pool)
    .await
    .unwrap();

    Ok(TodoSqlxMapper::pg_row_to_entity(&row))
  }
}
Enter fullscreen mode Exit fullscreen mode

I guess these repositories are should be quite clear enough as they implement base logic to manage sql data.

We going to finish, and lets try to make some requests via grpc cli.
In this example I will use the official grpc cli for macOS (brew install grpc), by default tonic supports the server reflection,
so you can use this cli without any problems.

Create the new todo:
$ grpc_cli call localhost:50051 Create "name: 'Read the book', description: 'I would like to read 10 pages'"

connecting to localhost:50051
Received initial metadata from server:
date : Sun, 12 Nov 2023 07:14:53 GMT
status_code: 200
message: "CREATED"
data {
  id: "112e6968-3424-4575-8bde-a16bcf64eeb6"
  name: "Read the book"
  description: "I would like to read 10 pages"
  created_at: "2023-11-12 14:14:53.945656"
  updated_at: "2023-11-12 14:14:53.945855"
}
Rpc succeeded with OK status
Enter fullscreen mode Exit fullscreen mode

Update the todo by id:
$ grpc_cli call localhost:50051 Update "id: '112e6968-3424-4575-8bde-a16bcf64eeb6', completed: true"

connecting to localhost:50051
Received initial metadata from server:
date : Sun, 12 Nov 2023 07:27:05 GMT
status_code: 200
message: "SUCCESS"
data {
  id: "112e6968-3424-4575-8bde-a16bcf64eeb6"
  name: "Read the book"
  description: "I would like to read 10 pages"
  completed: true
  created_at: "2023-11-12 14:14:53.945656"
  updated_at: "2023-11-12 14:26:48.589915"
}
Rpc succeeded with OK status
Enter fullscreen mode Exit fullscreen mode

Get paginated todo list:
$ grpc_cli call localhost:50051 GetPaginated "page: 0, limit: 10"

connecting to localhost:50051
Received initial metadata from server:
date : Sun, 12 Nov 2023 11:17:22 GMT
status_code: 200
message: "SUCCESS"
data {
  id: "c641207e-7c1f-40ec-9735-70b3944bb1b1"
  name: "Buy drinks"
  description: "Going to market and buy some drinks"
  created_at: "2023-11-12 14:18:04.789755"
  updated_at: "2023-11-12 14:18:04.789791"
}
data {
  id: "f65e81f8-1620-4c68-9b58-3936bd250b0f"
  name: "Check the mail"
  description: "Looking for new messages in gmail"
  created_at: "2023-11-12 14:15:43.271947"
  updated_at: "2023-11-12 14:15:43.272056"
}
data {
  id: "112e6968-3424-4575-8bde-a16bcf64eeb6"
  name: "Read the book"
  description: "I would like to read 10 pages"
  completed: true
  created_at: "2023-11-12 14:14:53.945656"
  updated_at: "2023-11-12 14:26:48.589915"
}
Rpc succeeded with OK status
Enter fullscreen mode Exit fullscreen mode

So, thats all.
I finish the minimal microservice in Rust without any complex frameworks and main goal about implementation of clear architecture was done.

I leave the link to github where you can discover all repo.

PS.
My way can be looks like something not finished but if you would like to dive into Rust with NodeJS background, I think this post will help ypu.

Github link to repo

cqrs Article's
30 articles in total
Favicon
CQRS — Command Query Responsibility Segregation — A Java, Spring, SpringBoot, and Axon Example
Favicon
An opinionated guide to Event Sourcing in Typescript. Kickoff
Favicon
The best way of implementing Domain-driven design, Clean Architecture, and CQRS
Favicon
Understanding the CQRS Pattern
Favicon
Missive.js
Favicon
Event-Driven Architecture, Event Sourcing, and CQRS: How They Work Together
Favicon
Mediator and CQRS with MediatR
Favicon
Implementing CQRS and Event Sourcing in .NET Core 8
Favicon
CQRS (Command Query Responsibility Segregation)
Favicon
Implementing CQRS and Event Sourcing in Distributed Systems
Favicon
CQRS and Mediator pattern
Favicon
Vertical Slice: Um Déjà Vu do CQRS
Favicon
Demystifying CQRS for Junior Developers: A Friendly Guide
Favicon
CQRS with Low-Code
Favicon
One of many ways to migrate from NodeJS to Rust
Favicon
Understanding CQRS Pattern: Pros, Cons, and a Spring Boot Example
Favicon
Integrating CQRS and Mediator in .NET with MediatR: An Elegant Convergence for Robust Applications
Favicon
Implementing CQRS in ASP.NET using MediatR
Favicon
CQRS Pattern With MediatR
Favicon
What is CQRS Pattern?
Favicon
Why do not you use IPipelineBehavior ?!
Favicon
Implementing the CQRS Pattern in NestJS with a Note API Example
Favicon
CQRS+
Favicon
CQRS and Event Sourcing for Software Architecture.
Favicon
Getting Started with Event Sourcing and EventSourcing.Backbone
Favicon
CQRS Pattern in Microservices Architecture
Favicon
Implementing CQRS in Python
Favicon
Easiest way to build the fastest REST API in C# and .NET 7 using CQRS
Favicon
Idempotent Consumer - Handling Duplicate Messages
Favicon
Notes on Microservices — Part 1

Featured ones: