Let's create a basic crud api with Rust using Tide
Tags: rust, tide
Published: 2021-07-11 19:14:42

Some time ago I started to learn about [rust-lang](https://www.rust-lang.org/) and after read and watch some stuffs I decided to give a try to create a basic CRUD using [tide](https://github.com/http-rs/tide) as a web framework.

This example ( and approach ) is heavy inspired in two awesome resources :

- [Zero to Production in Rust](https://www.lpalmieri.com/posts/2020-05-10-announcement-zero-to-production-in-rust/) by Luca Palmieri (@algo_luca)

- [Witter, a twitter clone in Rust](https://www.youtube.com/channel/UCDmSWx6SK0zCU2NqPJ0VmDQ)  by David Pedersen (@davidpdrsn)


---

Les start by creating a new `binary` project.
> If you don't have rust installed, please check the [install](https://www.rust-lang.org/tools/install) page and also can check my [notes](https://collectednotes.com/javier/rust-lang-notes) about `rustup` and `cargo`.

```bash
$ cargo new tide-basic-crud && cd tide-basic-crud
```

Next we want to add `tide` as dependency since is the `http framework` I selected to use, we can add it by hand editing the `
Cargo.toml` file or install [cargo-edit](https://github.com/killercup/cargo-edit) and use the `add` command.

```bash
$ cargo install cargo-edit
$ cargo add tide
    Updating 'https://github.com/rust-lang/crates.io-index' index
      Adding tide v0.13.0 to dependencies
```

Now following the [getting started](https://github.com/http-rs/tide#getting-started) guide of tide add `async-std` with the `attributes` feature enable by adding this line to your deps ( in `Cargo.toml` file ).

```
async-std = { version = "1.6.0", features = ["attributes"] }
```

And edit our `main.rs` with the basic example.

```rust
#[async_std::main]
async fn main() -> Result<(), std::io::Error> {
    tide::log::start();
    let mut app = tide::new();
    app.at("/").get(|_| async { Ok("Hello, world!") });
    app.listen("127.0.0.1:8080").await?;
    Ok(())
}
```

Now we can compile and run the server with

```bash
$ cargo run

[Running 'cargo run']
   Compiling tide-basic-crud v0.1.0 (/Users/pepo/personal/rust/tide-basic-crud)
    Finished dev [unoptimized + debuginfo] target(s) in 5.25s
     Running `target/debug/tide-basic-crud`
tide::log Logger started
    level Info
tide::listener::tcp_listener Server listening on http://127.0.0.1:8080
```

And check with `curl`

```bash
$ curl localhost:8080
Hello, world!
```

Awesome! we have our server up and running, but at this point doesn't do much so let's add some code. For the purpose of this example let's create a simple `CRUD` that allow us to track `dinosaurs` information.

We will recording the `name`, `weight` ( in kilograms ) and the `diet` ( type ).
First let's create a route for create a `dinos`, adding the `/dinos` route ( at ) with the verb `post` and following the request/response [concept](https://docs.rs/tide/0.13.0/tide/#concepts)

```rust
(...)
    app.at("/dinos").post(|mut req: Request<()>| async move {
        let body = req.body_string().await?;
        println!("{:?}", body);
        let mut res = Response::new(201);
        res.set_body(String::from("created!"));
        Ok(res)
    });

```

And test...

```bash
$ curl -v -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos

(...)
* upload completely sent off: 59 out of 59 bytes
< HTTP/1.1 201 Created
(...)
created!
```

And we can check the payload of the request in the `logs`

```bash
tide::log::middleware <-- Request received
    method POST
    path /dinos
"{\"name\":\"velociraptor\", \"weight\": 50, \"diet\":\"carnivorous\"}"
tide::log::middleware --> Response sent
    method POST
    path /dinos
    status 201
    duration 59.609µs
```

Nice! But we get the body `as string` and we need to parse as `json`.  If you are familiarized with `node.js` and `express` this could be done with the `body-parser` middleware, but `tide` can parse `json` and `form` (urlencoded) out of the box with `body_json` and `body_form` methods.

Let's change `body_string()` to `body_json` and try again.

```bash
curl -v -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos

< HTTP/1.1 422 Unprocessable Entity
```

`422 Unprocessable Entity`, doesn't works as expected (or maybe yes). `Tide` use `serde` to deserialize the request body and need to be parse `into` a struct.  So, let's create our `Dino` struct and deserialize the body into.

```rust
#[derive(Debug, Deserialize, Serialize)]
struct Dino {
    name: String,
    weight: u16,
    diet: String
}
```

Here we use the `derive` attributes to all to `serialize/deserialize`, now let's change the route to deserialize the body into the the `Dino` struct and return the `json` representation.

```rust
    app.at("/dinos").post(|mut req: Request<()>| async move {
        let  dino: Dino = req.body_json().await?;
        println!("{:?}", dino);
        let mut res = Response::new(201);
        res.set_body(Body::from_json(&dino)?);
        Ok(res)
    });
```

And check if works as expected...

```bash
$ curl -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos

{"name":"velociraptor","weight":50,"diet":"carnivorous"}
```

Nice! let's move forward and store those `dinos` using a `hashMap` to store a `key/value` in memory. We will add a db persistence later.

Looking the `tide` documentation, we can use `tide::`[with_state](https://docs.rs/tide/0.13.0/tide/fn.with_state.html) to create a server with shared application scoped state.

Let's use the state to store our `hashMap`.


```rust
#[derive(Clone,Debug)]
struct State {
    dinos: Arc<RwLock<HashMap<String,Dino>>>
}

```

We `wrap` our hashMap in a mutex here to use in the `State` ( thanks to the tide awesome community for the tip ).

Then in the main `fn`

```rust
    let state = State {
        dinos: Default::default()
    };

    let mut app = tide::with_state(state);
    app.at("/").get(|_| async { Ok("Hello, world!") });

    app.at("/dinos")
       .post(|mut req: Request<State>| async move {
           let  dino: Dino = req.body_json().await?;
           // let get a mut ref of our store ( hashMap )
           let mut dinos = req.state().dinos.write().await;
           dinos.insert(String::from(&dino.name), dino.clone());
           let mut res = Response::new(201);
           res.set_body(Body::from_json(&dino)?);
          Ok(res)
     })
```

Nice, let's add a route to `list` the `dinos` and check how it's works

```rust
    app.at("/dinos")
        .get(|req: Request<State>| async move {
            let dinos = req.state().dinos.read().await;
            // get all the dinos as a vector
            let dinos_vec: Vec<Dino> = dinos.values().cloned().collect();
            let mut res = Response::new(200);
            res.set_body(Body::from_json(&dinos_vec)?);
            Ok(res)
        })
```

```bash
$ curl -d '{"name":"velociraptor", "weight": 50, "diet":"carnivorous"}' http://localhost:8080/dinos

$ curl -d '{"name":"t-rex", "weight": 5000, "diet":"carnivorous"}' http://localhost:8080/dinos

$ curl http://localhost:8080/dinos

[{"name":"velociraptor","weight":50,"diet":"carnivorous"},{"name":"t-rex","weight":5000,"diet":"carnivorous"}]
```

Nice! now let's try getting and individual `dino`...

```rust
    app.at("/dinos/:name")
        .get(|req: Request<State>| async move {
            let mut dinos = req.state().dinos.write().await;
            let key: String = req.param("name")?;
            let res = match dinos.entry(key) {
                Entry::Vacant(_entry) => Response::new(404),
                Entry::Occupied(entry) => {
                    let mut res = Response::new(200);
                    res.set_body(Body::from_json(&entry.get())?);
                    res
                }
            };
            Ok(res)
        })
```

We are using the `entry` api so, before using here we need to bring it to the scope. We can do it adding this line at the top of the file

```rust
use std::collections::hash_map::Entry;
```

Now we can `match` in the get to return the `dino` or `404` if the requested name doesn't exists.

```bash
$ curl http://localhost:8080/dinos/t-rex

{"name":"t-rex","weight":5000,"diet":"carnivorous"}

$ curl -I http://localhost:8080/dinos/trex
HTTP/1.1 404 Not Found
content-length: 0
```

Awesome! We are almost there, add the missing routes to complete the `CRUD`.

```rust
        .put(|mut req: Request<State>| async move {
            let dino_update: Dino = req.body_json().await?;
            let mut dinos = req.state().dinos.write().await;
            let key: String = req.param("name")?;
            let res = match dinos.entry(key) {
                Entry::Vacant(_entry) => Response::new(404),
                Entry::Occupied(mut entry) => {
                    *entry.get_mut() = dino_update;
                    let mut res = Response::new(200);
                    res.set_body(Body::from_json(&entry.get())?);
                    res
                }
            };
            Ok(res)
        })
        .delete(|req: Request<State>| async move {
            let mut dinos = req.state().dinos.write().await;
            let key: String = req.param("name")?;
            let deleted = dinos.remove(&key);
            let res = match deleted {
                None => Response::new(404),
                Some(_) => Response::new(204),
            };
            Ok(res)
        });

```

Let's try update...

```bash
$ curl -v -X PUT -d '{"name":"t-rex", "weight": 5, "diet":"carnivorous"}' http://localhost:8080/dinos/t-rex

$ curl http://localhost:8080/dinos/t-rex

{"name":"t-rex","weight":5,"diet":"carnivorous"}
```

and delete

```bash
$ curl -v -X DELETE  http://localhost:8080/dinos/t-rex

$ curl -I http://localhost:8080/dinos/t-rex
HTTP/1.1 404 Not Found
```

We made it! a complete CRUD with tide. But that was a `lot` of manual testing, let's add some basic unit test to smooth the next steps.
First decouple our `main` function of the `server` creation allowing us to create a server without need to actually listen in any port.

```rust
#[async_std::main]
async fn main() {
    tide::log::start();
    let dinos_store = Default::default();
    let app = server(dinos_store).await;

    app.listen("127.0.0.1:8080").await.unwrap();
}

async fn server(dinos_store: Arc<RwLock<HashMap<String, Dino>>>) -> Server<State> {
    let state = State {
        dinos: dinos_store, //Default::default(),
    };

    let mut app = tide::with_state(state);
    app.at("/").get(|_| async { Ok("ok") });

(...)
app
```

Great, now we can add some basic test using the `server` function using `cargo` for running our tests. There is more information about tests in the [cargo book](https://doc.rust-lang.org/cargo/guide/tests.html) but in general

> Cargo can run your tests with the cargo test command. Cargo looks for tests to run in two places: in each of your src files and any tests in tests/. Tests in your src files should be unit tests, and tests in tests/ should be integration-style tests. As such, you’ll need to import your crates into the files in tests.

```rust
#[async_std::test]
async fn list_dinos() -> tide::Result<()> {
    use tide::http::{Method, Request, Response, Url};

    let dino = Dino {
        name: String::from("test"),
        weight: 50,
        diet: String::from("carnivorous"),
    };

    let mut dinos_store = HashMap::new();
    dinos_store.insert(dino.name.clone(), dino);
    let dinos: Vec<Dino> = dinos_store.values().cloned().collect();
    let dinos_as_json_string = serde_json::to_string(&dinos)?;

    let state = Arc::new(RwLock::new(dinos_store));
    let app = server(state).await;

    let url = Url::parse("https://example.com/dinos").unwrap();
    let req = Request::new(Method::Get, url);
    let mut res: Response = app.respond(req).await?;
    let v = res.body_string().await?;
    assert_eq!(dinos_as_json_string, v);
    Ok(())
}

#[async_std::test]
async fn create_dino() -> tide::Result<()> {
    use tide::http::{Method, Request, Response, Url};

    let dino = Dino {
        name: String::from("test"),
        weight: 50,
        diet: String::from("carnivorous"),
    };

    let dinos_store = HashMap::new();

    let state = Arc::new(RwLock::new(dinos_store));
    let app = server(state).await;

    let url = Url::parse("https://example.com/dinos").unwrap();
    let mut req = Request::new(Method::Post, url);
    req.set_body(serde_json::to_string(&dino)?);
    let res: Response = app.respond(req).await?;
    assert_eq!(201, res.status());
    Ok(())
}

```
*This unit tests are inspired in this [gist](https://gist.github.com/BillBarnhill/ca9bb3b58bdc57a632613546d070222e) that was posted on the `tide-users` discord channel.*

The main idea here is that we can create our server ( `app` ), calling the `endpoint` with a `req`uest without need to make an actual `http request` and have the server `listen` in any port.

We can now run `cargo test`

```bash
$  cargo test
   Compiling tide-basic-crud v0.1.0 (/Users/pepo/personal/rust/tide-basic-crud)
    Finished test [unoptimized + debuginfo] target(s) in 5.99s
     Running target/debug/deps/tide_basic_crud-3d6db2bae3cd08a5

running 5 tests
test list_dinos ... ok
test index_page ... ok
test create_dino ... ok
test delete_dino ... ok
test update_dino ... ok

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

Just one more think to add, `cargo` is an awesome tool and you can add more functionality like `watching` changes in you code and react ( for example running test ) with [cargo-watch](https://github.com/passcod/cargo-watch).


This is all for now, my intent to learn by doing. I'm sure that there is other ways ( likely  betters ) to create a `crud` with `tide` and any feedback is welcome :) .

 In the next post I will refactor the code to move the `dinos_store` to a database ( postgresql / sqlx ).


You can find the complete code in this [repo](https://github.com/pepoviola/tide-basic-crud) Thanks!