Rocket Tutorial 02: Minimalist API
Tags: rust, beginners, tutorial
Published: 2020-11-10 14:13:52

[ <span>Photo by <a href="https://unsplash.com/@freeshype?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Adetunji Paul</a> on <a href="https://unsplash.com/s/photos/paper-plane?utm_source=unsplash&amp;utm_medium=referral&amp;utm_content=creditCopyText">Unsplash</a></span> ]

In this installment we will create a minimalist CRUD API server using Rocket. Actually, we will just sketch one up, and prepare the foundations for more to come.

The code for this tutorial can be found in this repository: [github.com/davidedelpapa/rocket-tut](https://github.com/davidedelpapa/rocket-tut), and has been tagged for your convenience:

```bash
git clone https://github.com/davidedelpapa/rocket-tut.git
cd rocket-tut
git checkout tags/tut2
```

## Clean up -- Refactor

As first thing to do, we will refactor the code.

This is the filesystem we will end up with after refactoring:

```
.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
│   ├── lib.rs
│   ├── main.rs
│   └── routes
│       ├── echo.rs
│       └── mod.rs
├── static
│   └── abruzzo.png
└── tests
    └── basic_test.rs
```

In *src/* we will create the folder *routes/*, where our routes will go, while outside *src/*, alongside it, we will create a *tests/* folder to hold all our tests

Let's go into all the files needed.

Alongside *main,rs*, inside *src/* we will create a *lib.rs* with the following code:

```rust
#![feature(proc_macro_hygiene, decl_macro)]
#![allow(unused_attributes)]

#[macro_use] use rocket::*;
use rocket_contrib::serve::StaticFiles;
use rocket_contrib::helmet::SpaceHelmet;

mod routes;

pub fn rocket_builder() -> rocket::Rocket {
    rocket::ignite().attach(SpaceHelmet::default())
    .mount("/", routes![routes::echo::echo_fn])
 
    .mount("/files", StaticFiles::from("static/"))
}
```

As you can see, we refactored the `fn rocket()`, changing its name to `rocket_builder()` in order that it will not collide with the *Rocket* crate.

We declare a `mod routes` inside this file as well. Notice too that the namespace for the route of our echo function is now `routes::echo::echo_fn`.

In order to use the directory *src/routes/* as a module, we will create a *mod.rs* inside it, with the following content

```rust
pub mod echo;
```

Then we will create the file *src/routes/echo.rs* with the following content:

```rust
use rocket::*;

#[get("/echo/<echo>")]
pub fn echo_fn(echo: String) -> String {
    echo
}
```

This is simply our `echo_fn()` factored out.

Finally our *src/main.rs* will be just as follows:

```rust
use rocket_tut::rocket_builder;

fn main() {

    rocket_builder().launch();
}
```

Everything has been factored out, that is why it feels so bare-bones.

With `cargo run` we will insure everything is working properly.

Lastly, inside *tests/* we will create all our tests for the server. Thus,let's start with the code we already have, that we will put inside *tests/basic_test.rs*:

```rust
use rocket::local::Client;
use rocket::http::Status;
use rocket_tut::rocket_builder;

#[test]
fn echo_test() {
    let client = Client::new(rocket_builder()).expect("Valid Rocket instance");
    let mut response = client.get("/echo/test_echo").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.body_string(), Some("test_echo".into()));
}
```

The code for testing has been reduced as well.

With `cargo test` we can confirm that everything is testing properly.

## The bare minimum for a usable API

We will create a [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) server, with the capability of registering a user, and managing the user account.

We will have the following endpoints, each with its method:

GET /users/ - Get a list of all the registered users
POST /users/ - registers a new user
GET /users/<id> - Get info on the user with id = <id>
PUT /users/<id> - Updates info of the user with id = <id>
DELETE /users/<id> - Deletes the user with id = <id>

For now we will keep it that simple.

As for the `echo_fn()`, we will not need it, so we will transform it from a *echo* to a *ping* function. This is useful to check if the server's up and running, in case anybody needs it.

*src/routes/echo.rs* will become *src/routes/ping.rs*, with the following content:

```rust
use rocket::*;

#[get("/ping")]
pub fn ping_fn() -> String {
    "PONG!".to_string()
}
```

We need to update *src/routes/mod.rs* to point to the new file:

```rust
pub mod ping;
```

And also *src/lib.rs*:

```rust
.mount("/", routes![routes::ping::ping_fn])
```

Finally, the *tests/basic_test.rs* has to reflect the new route & test condition:

```rust
#[test]
fn ping_test() {
    let client = Client::new(rocket_builder()).expect("Valid Rocket instance");
    let mut response = client.get("/ping").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.body_string(), Some("PONG!".into()));
}
```

Now both `cargo test` and `cargo run` should work as expected.

## The User's routes

Let's add a *user.rs* inside *src/routes*.

We will use placeholders routes just in order to scaffold and quick-test this.

```rust
use rocket::*;

#[get("/users")]
pub fn user_list_rt() -> String {
    "List of users".to_string()
}

#[post("/users")]
pub fn new_user_rt() -> String {
    "Creation of new user".to_string()
}

#[get("/users/<id>")]
pub fn info_user_rt(id: String) -> String {
    format!("Info for user {}", id)
}

#[put("/users/<id>")]
pub fn update_user_rt(id: String) -> String {
    format!("Update info for user {}", id)
}

#[delete("/users/<id>")]
pub fn delete_user_rt(id: String) -> String {
    format!("Delete user {}", id)
}
```

As you can see, there is a function (ending in `_rt`) for each route, with the agreed upon route and http method.

We need to update *src/routes/mod.rs*

```rust
pub mod ping;
pub mod user;
```

as well as *src/lib.rs*:

```rust
pub fn rocket_builder() -> rocket::Rocket {
    rocket::ignite().attach(SpaceHelmet::default())
    .mount("/", routes![routes::ping::ping_fn])
    .mount("/api", routes![
        routes::user::user_list_rt,
        routes::user::new_user_rt,
        routes::user::info_user_rt,
        routes::user::update_user_rt,
        routes::user::delete_user_rt
    ])
    .mount("/files", StaticFiles::from("static/"))
}
```

As you can see, we added all the routes under the `/api` mount-point. This way, the GET users/<id>, can be retrieved with: [localhost:8000/api/users/1](http://127.0.0.1:8000/api/users/1) for example.

To test this now we will go with an empirical method, checking the API manually.

In this case I am using [ARC for Google Chrome](https://chrome.google.com/webstore/detail/advanced-rest-client/hgmloofddffdnphfgcellkdfbfbjeloo), but there are many alternatives, such as Postman, Insomnia, Hoppscotch.io (formerly postwoman), etc. Even `curl` can be used for the purpose!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/7nunha1asl2ydxf6pu86.jpg)

## JSON responses

After a quick test, it is time to transform our REST API for the better, so that it can respond with JSON

First we need [serde](https://serde.rs/):

```bash
cargo add serde --features derive
```

Now let's change the code in *src/routes/user.rs*. We'll add a struct to represent the JSON response.

```rust
use serde::{Deserialize, Serialize};


#[derive(Serialize, Deserialize)]
pub struct Response {
    status: String,
    message: String,
}
impl Response {
    fn ok(msg: &str) -> Self {
        Response {
            status: "Success".to_string(),
            message: msg.to_string(),
        }
    }
    fn err(msg: &str) -> Self {
        Response {
            status: "Error".to_string(),
            message: msg.to_string(),
        }
    }
}

```

Now we have a `Response` struct that will be converted to a JSON object. We implemented two functions to return one a message with `Success`, and the other a message with an `Error`.

Now let's change accordingly the first route, `fn user_list_rt()`:

```rust
#[get("/users")]
pub fn user_list_rt() -> Json<Response> {
    Json(Response::ok("List of users"))
}
```

If we test empirically the response, we see it working.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/at7q5j425c62t3wa3isv.jpg)

Now we can now update all the routes the same way:

```rust
#[post("/users")]
pub fn new_user_rt() -> Json<Response> {
    Json(Response::ok("Creation of new user"))
}

#[get("/users/<id>")]
pub fn info_user_rt(id: String) -> Json<Response> {
    Json(Response::ok(&* format!("Info for user {}", id)))
}

#[put("/users/<id>")]
pub fn update_user_rt(id: String) -> Json<Response> {
    Json(Response::ok(&* format!("Update info for user {}", id)))
}

#[delete("/users/<id>")]
pub fn delete_user_rt(id: String) -> Json<Response> {
    Json(Response::ok(&* format!("Delete user {}", id)))
}
```

## Implementing the Tests

In order to implement the tests we will create a mod inside the *tests/* folder, to collect all initialization logic we will need for our tests.

This will be very helpful later on as well.

For now let's create the directory *tests/common/*, inside which we will write the following *mod.rs*:

```rust
use rocket::local::Client;
use rocket_tut::rocket_builder;

pub fn setup () -> Client {
    Client::new(rocket_builder()).expect("Valid Rocket instance")
}
```

This will ensure we can call the `init` of the Rocket client from all our tests, as well as giving us the possibility to insert more needed init code for the future.

Next we will modify our *tests/basic_test.rs* as follows:

```rust
use rocket::http::{ContentType, Status};

mod common;

#[test]
fn ping_test() {
    let client = common::setup();
    let mut response = client.get("/ping").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.body_string(), Some("PONG!".into()));
}
```

Now we can use the `setup()` function we defined in *tests/common/mod.rs*.

Going further, we can check also the first route we wrote in *src/routes/user.rs*, the `user_list_rt()` function.

The following is the code to test it:

```rust
#[test]
fn user_list_rt_test(){
    let client = common::setup();
    let mut response = client.get("/api/users").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(response.body_string(), Some("{\"status\":\"Success\",\"message\":\"List of users\"}".into()));
}
```

As you can see we check the success of the response, but we test as well that the response corresponds to a `application/json` in the Content-Type, and finally we check for the actual response as well (we construct the JSON of the body directly as a String).

Now running `cargo test` should verify that all the tests pass successfully.

We can finally test the whole set of routes, to ensure everything works as expected.

```rust
#[test]
fn new_user_rt_test(){
    let client = common::setup();
    let mut response = client.post("/api/users").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(response.body_string(), Some("{\"status\":\"Success\",\"message\":\"Creation of new user\"}".into()));
}

#[test]
fn info_user_rt_test(){
    let client = common::setup();
    let mut response = client.get("/api/users/1").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(response.body_string(), Some("{\"status\":\"Success\",\"message\":\"Info for user 1\"}".into()));
}

#[test]
fn update_user_rt_test(){
    let client = common::setup();
    let mut response = client.put("/api/users/1").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(response.body_string(), Some("{\"status\":\"Success\",\"message\":\"Update info for user 1\"}".into()));
}

#[test]
fn delete_user_rt_test(){
    let client = common::setup();
    let mut response = client.delete("/api/users/1").dispatch();
    assert_eq!(response.status(), Status::Ok);
    assert_eq!(response.content_type(), Some(ContentType::JSON));
    assert_eq!(response.body_string(), Some("{\"status\":\"Success\",\"message\":\"Delete user 1\"}".into()));
}
```

## Conclusions

We started to stub out our CRUD server.

Of course the routes still do not work as intended, and the tests are merely a pretext, but hey!, everything works correctly, and we are setting the environment for more to come.

Stay tuned!