Basic CRUD with rust using tide - Refactor and complete CI/CD
Tags: rust, tide, sqlx, api
Published: 2021-07-19 09:17:22

Welcome back! this will be the last `post` of this serie, making the final refactor to our application and tiding the loose ends to complete the `ci/cd` setup.

###  Refactor

Let's start to tackle the issue prompted in the las [post](https://javierviola.com/post/05-basic-crud-with-rust-using-tide-front-end-with-tera/)

> but we are starting to have some duplicate code between the endpoints > for the rest api and the endpoint for some pages.

Since we added new `endpoints`, for each page, we dup some code to get the information from the database. We can resolve this decoupling the that logic from the endpoints and creating a new reusable layer.

The final goal is two have two separate layers, with a clear separation of roles and responsibilities between each one. For the purpose of this post we are going to use two layers, one for controllers and one for the `business` or `domain` logic.

So, let's start with the controller layer creating a new directory inside `src` and two controller files. One for the views ( or pages ) and the other one for the `api`.

```bash
mkdir controllers && cd controllers
touch {mod.rs,dino.rs,views.rs}
```
Now we can move the `endpoint` function to each one and declare the modules in our `mod` file.

```rust
// dino.rs

pub async fn create(mut req: Request<State>) -> tide::Result {
    todo!();
}

pub async fn list(req: tide::Request<State>) -> tide::Result {
    todo!();
}

pub async fn get(req: tide::Request<State>) -> tide::Result {
    todo!();
}

pub async fn update(mut req: tide::Request<State>) -> tide::Result {
    todo!();
}

pub async fn delete(req: tide::Request<State>) -> tide::Result {
    todo!();
}
```

Great!, the idea behind this refactor is decoupling the `endpoint` logic to the one used to interact to the database. So, this functions will receive the request and call the other layer ( let's call `handler` ) to make crud operations.

Now we need to create the new `handler` layer and expose the api to be used by the `controllers`. So again inside our `src` directory.

```bash
mkdir handlers && cd handlers
touch {mod.rs,dino.rs}
```

```rust
// handlers/dino.rs
pub async fn create(dino: Dino, db_pool: PgPool) -> tide::Result<Dino> {
    todo!();
}
pub async fn list(db_pool: PgPool) -> tide::Result<Vec<Dino>> {
    todo!();
}

pub async fn get(id: Uuid, db_pool: PgPool) -> tide::Result<Option<Dino>> {
    todo!();
}
pub async fn delete(id: Uuid, db_pool: PgPool) -> tide::Result<Option<()>> {
    todo!();
}

pub async fn update(id: Uuid, dino: Dino, db_pool: PgPool) -> tide::Result<Option<Dino>> {
    todo!();
}
```

Each function will receive as argument an instance of the database pool and the needed arguments for the operation ( e.g uuid or dino ).

Now we can start filling the `todo!()` and implement the actual logic, here is an example of the `create` operation but you can check the [PR](https://github.com/pepoviola/tide-basic-crud/pull/6) to see the full code.

```rust
// controllers/dino.rs

use crate::handlers;

pub async fn create(mut req: Request<State>) -> tide::Result {
    let dino: Dino = req.body_json().await?;
    let db_pool = req.state().db_pool.clone();

    let row = handlers::dino::create(dino, db_pool).await?;

    let mut res = Response::new(201);
    res.set_body(Body::from_json(&row)?);
    Ok(res)
}
```

```rust
pub async fn create(dino: Dino, db_pool: PgPool) -> tide::Result<Dino> {
    let row: Dino = query_as!(
        Dino,
        r#"
        INSERT INTO dinos (id, name, weight, diet) VALUES
        ($1, $2, $3, $4) returning id, name, weight, diet
        "#,
        dino.id,
        dino.name,
        dino.weight,
        dino.diet
    )
    .fetch_one(&db_pool)
    .await
    .map_err(|e| Error::new(409, e))?;

    Ok(row)
}
```

Great! After complete the implementation of the others functions we can move on and complete our view controller.

We have 3 views ( index, new and edit )
```rust
// controllers/views.rs

pub async fn index(req: Request<State>) -> tide::Result {
    let tera = req.state().tera.clone();
    let db_pool = req.state().db_pool.clone();
    let rows = handlers::dino::list(db_pool).await?;

    tera.render_response(
        "index.html",
        &context! {
           "title" => String::from("Tide basic CRUD"),
           "dinos" => rows
        },
    )
}

pub async fn new(req: Request<State>) -> tide::Result {
    let tera = req.state().tera.clone();

    tera.render_response(
        "form.html",
        &context! {
            "title" => String::from("Create new dino")
        },
    )
}

pub async fn edit(req: Request<State>) -> tide::Result {
    let tera = req.state().tera.clone();
    let db_pool = req.state().db_pool.clone();
    let id: Uuid = Uuid::parse_str(req.param("id")?).unwrap();
    let row = handlers::dino::get(id, db_pool).await?;

    let res = match row {
        None => Response::new(404),
        Some(row) => {
            let mut r = Response::new(200);
            let b = tera.render_body(
                "form.html",
                &context! {
                    "title" => String::from("Edit dino"),
                    "dino" => row
                },
            )?;
            r.set_body(b);
            r
        }
    };

    Ok(res)
}
```

Awesome! each one now call the `handler` to retrieve the `dinos` information. We are almost there, we need now to clean our main file and add tide the routes to the controllers. We can start by removing all the `restEntity` logic in main and just tide the routes to the controller functions in our server `fn`

```rust
    // views
    app.at("/").get(views::index);
    app.at("/dinos/new").get(views::new);
    app.at("/dinos/:id/edit").get(views::edit);

    // api
    app.at("/dinos").get(dino::list).post(dino::create);

    app.at("dinos/:id")
        .get(dino::get)
        .put(dino::update)
        .delete(dino::delete);

    app.at("/public")
        .serve_dir("./public/")
        .expect("Invalid static file directory");

    app
```

Great! so, we can now run the `tests`

```bash
cargo test

running 10 tests
test tests::clear ... ok
test tests::delete_dino ... ok
test tests::create_dino_with_existing_key ... ok
test tests::delete_dino_non_existing_key ... ok
test tests::create_dino ... ok
test tests::get_dino ... ok
test tests::update_dino ... ok
test tests::get_dino_non_existing_key ... ok
test tests::list_dinos ... ok
test tests::updatet_dino_non_existing_key ... ok

test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
```

Nice! we almost finish the refactor, we still have some code to clean and remove unused `imports`.


### CI/CD

Next, we need to finish the pipeline for CI/CD, we are already running the test in GH Actions so this part is cover but we want to setup the `cd` part for deploy our app in each commit to `main`.

#### Updating sqlx
First let's start to update `sqlx` to enable `offline` mode and allow us to build our app without connecting to the database.

```toml
// Cargo.toml
sqlx = { version = "0.4.2", features = ["runtime-async-std-rustls", "offline", "macros", "chrono", "json", "postgres", "uuid"] }
```
We need to add the `offline` feature and change `runtime-async-std` for one of the supported ones in the new version ( in our case `runtime-async-std-rustls` ).

Also, we need to make a couple of changes in our code to work with the new version:

- Pool::new is now replaced by Pool::connect

```rust
pub async fn make_db_pool(db_url: &str) -> PgPool {
    Pool::connect(db_url).await.unwrap()
}
```

- We need to use `as "id!"` in the query for inserting a Dino to prevent the inferred field type (Option<String>)

```rust
// use as "id!" here to prevent sqlx use the inferred ( nulleable ) type
// since we know that id should not be null
// see : https://github.com/launchbadge/sqlx/blob/master/src/macros.rs#L482
pub async fn create(dino: Dino, db_pool: &PgPool) -> tide::Result<Dino> {
    let row: Dino = query_as!(
        Dino,
        r#"
        INSERT INTO dinos (id, name, weight, diet) VALUES
        ($1, $2, $3, $4) returning id as "id!", name, weight, diet
        "#,
        dino.id,
        dino.name,
        dino.weight,
        dino.diet
    )
    .fetch_one(db_pool)
    .await
    .map_err(|e| Error::new(409, e))?;

    Ok(row)
}
```

Great! now we can install the `sqlx-cli` and generate the needed file to build without need a db connection.

```bash
cargo install sqlx-cli
cargo sqlx prepare
(...)
query data written to `sqlx-data.json` in the current directory; please check this into version control
```

Nice! we can now build our code with out a db connection :)


#### Selecting a provider

For deploy our app we will use [qovery](https://qovery.com/), they offer container as a service and his tag line are `The simplest way to deploy your full-stack apps`. Also, they have a `community` plan so let's try and see how it's go.

For deploy our app we need to files:

 - Dockerfile : for build and run our application
 - .qovery.yml  : fir describe the needed infra of our project.

For the Dockerfile we will use a template from [cargo chef](https://crates.io/crates/cargo-chef) enabling multi-stage builds.

You can check the complete files in the [repo](https://github.com/pepoviola/tide-basic-crud/) .

Now, we can see the deployment in action in the [qovery](https://qovery.com) console

![qovery console](https://user-images.githubusercontent.com/363911/103180754-a31fa400-4877-11eb-9e08-af6d2828971c.png)

![image](https://user-images.githubusercontent.com/363911/103183367-ba1ec000-4890-11eb-8f6b-329e09128fdd.png)

And each commit will trigger a new build of our app, that is now [live](https://main-mi67osvs46wv2llq-gtw.qovery.io/) :-)

![image](https://user-images.githubusercontent.com/363911/103184280-2819b600-4896-11eb-8e07-1b87f3b7ba34.png)


---
That's all for today and for this series, I think there are still room to improve but we can leave that for the future since we have now a minimal working CRUD with both api and front-end and the CI/CD in place :-)

In the future posts I will be exploring [web-sockets](https://github.com/http-rs/tide-websockets) with `tide`.

As always, I write this as a learning journal and there could be another more elegant and correct way to do it and any feedback is welcome.

Thanks!