Practical Rust Web Development - API Rest
Tags: rust, web, actixweb, diesel
Published: 2019-05-14 12:26:27

---
title: Practical Rust Web Development - API Rest
published: true
description: This is a series of posts to explore the possibilities of using Rust for web development
series: Practical Rust Web Development
tags: Rust, Web, Actix-Web, diesel
---

Update: According to this [issue](https://github.com/diesel-rs/diesel/issues/399) async does not work with Diesel, so, the method `to_async` from `web::get` might not work as expected, it will work but not the way you want, so, to be honest you might change it to `to`.

This is the first of a series of blog posts that shows how to use Rust for web development, I try to be as practical as possible, using tools already chosen for the job.

I'll start with the basics until we can create basic API Rest endpoints that lists, creates, edit and delete products from a fictitious web store.

I'll go step by step, however it's a good idea to check out the [Rust book](https://doc.rust-lang.org/book/) before to know a little bit about the Rust language.

The first thing we need to do is install Rust, we can go to https://www.rust-lang.org/tools/install and follow the instructions, because I use linux the example codes are taken from that OS, however you can try it with Windows or Mac.

Execute the next in a terminal and follow the instructions: `curl https://sh.rustup.rs -sSf | sh`

You can verify Rust is installed correctly by running `rustc -V`, it will show you the rustc version installed.

The next thing we're going to do is to create a new project, we can call it mystore, run the next in a terminal window: `cargo new mystore --bin`.

If everything were right we'll be able to see a folder with mystore name, we can see the basic structure of a Rust project:

![tree-mystore](https://i.ibb.co/56Gjg3R/tree-mystore.jpg)

The next thing we're going to need is a web framework, we'll use actix-web, a high level framework based on actix, an actor framework. Add the next lines of code in `cargo.toml`:

```toml
[dependencies]
actix = "0.8"
actix-web = "1.0.0-beta"
```
Now, when you execute `cargo build` the crate will be installed and the project will be compiled.

We'll start with a hello world example, add the next lines of code in `src/main.rs`:

```rust
extern crate actix_web;
use actix_web::{HttpServer, App, web, HttpRequest, HttpResponse};

// Here is the handler, 
// we are returning a json response with an ok status 
// that contains the text Hello World
fn index(_req: HttpRequest) -> HttpResponse  {
    HttpResponse::Ok().json("Hello world!")
}

fn main() {
    // We are creating an Application instance and 
    // register the request handler with a route and a resource 
    // that creates a specific path, then the application instance 
    // can be used with HttpServer to listen for incoming connections.
    HttpServer::new(|| App::new().service(
             web::resource("/").route(web::get().to_async(index))))
        .bind("127.0.0.1:8088")
        .unwrap()
        .run();
}

```

Execute `cargo run` in a terminal, then go to http://localhost:8088/ and see the result, if you can see the text Hello world! in the browser, then everything worked as expected.

Now, we're going to choose the database driver, in this case will be [diesel](http://diesel.rs/), we add a dependency in `Cargo.toml`:

```toml
[dependencies]
diesel = { version = "1.0.0", features = ["postgres"] }
dotenv = "0.9.0"
``` 

If we execute `cargo build` the crate will be installed and the project will be compiled.

It's a good idea to install the cli tool as well, `cargo install diesel_cli`.

If you run into a problem installing diesel_cli, it's probably because of a lack of the database driver, so, make sure to include them, if you use Ubuntu you might need to install `postgresql-server-dev-all`.

Execute the next command in bash:

```bash
$ echo DATABASE_URL=postgres://postgres:@localhost/mystore > .env
```

Now, everything is ready for diesel to setup the database, run `diesel setup` to create the database. If you run into problems configuring postgres, take a look into this [guide](https://www.cyberciti.biz/faq/postgresql-remote-access-or-connection).

Let's create a table to handle products: 

```bash
diesel migration generate create_products
```

Diesel CLI will create two migrations files, `up.sql` and `down.sql`.

`up.sql`:

```sql
CREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name VARCHAR NOT NULL,
  stock FLOAT NOT NULL,
  price INTEGER --representing cents
)
```

`down.sql`:

```sql
DROP TABLE products
```

Applying the migration:

```bash
diesel migration run
```

We'll load the libraries we're going to need in `main.rs`:

`src/main.rs`:

```rust
#[macro_use]
extern crate diesel;
extern crate dotenv;
```

Next we create a file to handle database connections, let's call it db_connection.rb and save it in src.


`src/db_connection.rs`:

```rust
use diesel::prelude::*;
use diesel::pg::PgConnection;
use dotenv::dotenv;
use std::env;

pub fn establish_connection() -> PgConnection {
    dotenv().ok(); // This will load our .env file.

    // Load the DATABASE_URL env variable into database_url, in case of error
    // it will through a message "DATABASE_URL must be set"
    let database_url = env::var("DATABASE_URL")
        .expect("DATABASE_URL must be set");
    
    // Load the configuration in a postgres connection, 
    // the ampersand(&) means we're taking a reference for the variable. 
    // The function you need to call will tell you if you have to pass a
    // reference or a value, borrow it or not.
    PgConnection::establish(&database_url)
        .expect(&format!("Error connecting to {}", database_url))
}
```

Next, we're going to create our first resource. A list of products.

The first thing we need is a couple of structs, one for creating a resource, the other for getting the resource, in this case will be for products.

We can save them in a folder called models, but before that, we need a way to load our files, we add the next lines in `main.rs`:

`src/main.rs`:

```rust
pub mod schema;
pub mod models;
pub mod db_connection;
```

We need to create a file inside models folder, called `mod.rs`:

`src/models/mod.rs`:

```rust
pub mod product;
```

`src/models/product.rs`:

```rust
use crate::schema::products;

#[derive(Queryable)]
pub struct Product {
    pub id: i32,
    pub name: String,
    pub stock: f64,
    pub price: Option<i32> // For a value that can be null, 
                           // in Rust is an Option type that 
                           // will be None when the db value is null
}

#[derive(Insertable)]
#[table_name="products"]
pub struct NewProduct {
    pub name: Option<String>,
    pub stock: Option<f64>,
    pub price: Option<i32>
}
```

So, let's add some code to get a list of products, we'll create a new struct to handle the list of products called ProductList and add a function list to get products from the database, add the next block to `models/product.rs`:

```rust
// This will tell the compiler that the struct will be serialized and 
// deserialized, we need to install serde to make it work.
#[derive(Serialize, Deserialize)] 
pub struct ProductList(pub Vec<Product>);

impl ProductList {
    pub fn list() -> Self {
        // These four statements can be placed in the top, or here, your call.
        use diesel::RunQueryDsl;
        use diesel::QueryDsl;
        use crate::schema::products::dsl::*;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        let result = 
            products
                .limit(10)
                .load::<Product>(&connection)
                .expect("Error loading products");
        
        // We return a value by leaving it without a comma
        ProductList(result)
    }
}
```

I'm doing it this way so we can have freedom to add any trait to that struct, we couldn't do that for a Vector because we don't own it, ProductList is using the newtype pattern in Rust.

Now, we just need a handle to answer the request for a product lists, we'll use `serde` to serialize the data to a json response.

We need to edit `Cargo.toml`, `main.rs` and `models/product.rs`:

`Cargo.toml`:

```toml
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
```

`main.rs`:

```rust
pub mod handlers; // This goes to the top to load the next handlers module 

extern crate serde;
extern crate serde_json;
#[macro_use] 
extern crate serde_derive;
```

`src/models/product.rs`:

```rust
#[derive(Queryable, Serialize, Deserialize)]
pub struct Product {
    pub id: i32,
    pub name: String,
    pub stock: f64,
    pub price: Option<i32>
}
```

Add a file named `mod.rs` in `src/handlers`:

```rust
pub mod products;
```

We can create a file called `products.rs` in a handlers folder:

`src/handlers/products.rs`:

```rust
use actix_web::{HttpRequest, HttpResponse };

use crate::models::product::ProductList;

// This is calling the list method on ProductList and 
// serializing it to a json response
pub fn index(_req: HttpRequest) -> HttpResponse {
    HttpResponse::Ok().json(ProductList::list())
}
```

We need to add index handler to our server in `main.rs` to have a first part of the Rest API, update the file so it will look like this:

`src/main.rs`:

```rust
pub mod schema;
pub mod db_connection;
pub mod models;
pub mod handlers;

#[macro_use]
extern crate diesel;
extern crate dotenv;
extern crate serde;
extern crate serde_json;
#[macro_use] 
extern crate serde_derive;

extern crate actix;
extern crate actix_web;
extern crate futures;
use actix_web::{App, HttpServer, web};

fn main() {
    let sys = actix::System::new("mystore");

    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
        ))
    .bind("127.0.0.1:8088").unwrap()
    .start();

    println!("Started http server: 127.0.0.1:8088");
    let _ = sys.run();
}
```

Let's add some data and see what it looks like, in a terminal run:

```bash
psql -U postgres -d mystore -c "INSERT INTO products(name, stock, price) VALUES ('shoes', 10.0, 100); INSERT INTO products(name, stock, price) VALUES ('hats', 5.0, 50);"
```

Then execute:

```bash
cargo run
```

Finally goes to http://localhost:8088/products.

If everything is working as expected you should see a couple of products in a json value.


## Create a Product

Add `Deserialize` trait to `NewProduct` struct and a function to create products:

```rust
#[derive(Insertable, Deserialize)]
#[table_name="products"]
pub struct NewProduct {
    pub name: String,
    pub stock: f64,
    pub price: Option<i32>
}

impl NewProduct {
    
    // Take a look at the method definition, I'm borrowing self, 
    // just for fun remove the & after writing the handler and 
    // take a look at the error, to make it work we would need to use into_inner (https://actix.rs/api/actix-web/stable/actix_web/struct.Json.html#method.into_inner)
    // which points to the inner value of the Json request.
    pub fn create(&self) -> Result<Product, diesel::result::Error> {
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();
        diesel::insert_into(products::table)
            .values(self)
            .get_result(&connection)
    }
}
```

Then add a handler to create products:

```rust
use crate::models::product::NewProduct;
use actix_web::web;

pub fn create(new_product: web::Json<NewProduct>) -> Result<HttpResponse, HttpResponse> {
    
    // we call the method create from NewProduct and map an ok status response when
    // everything works, but map the error from diesel error 
    // to an internal server error when something fails.
    new_product
        .create()
        .map(|product| HttpResponse::Ok().json(product))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
```

Finally add the corresponding route and start the server:

`src/main.rs`:

```rust
    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        ))
    .bind("127.0.0.1:8088").unwrap()
    .start();
```

```bash
cargo run
```

We can create a new product:

```bash
curl http://127.0.0.1:8088/products \                                                                                                
        -H "Content-Type: application/json" \
        -d '{"name": "socks", "stock": 7, "price": 2}'

```

## Show a Product

`src/models/product.rs`:

```rust
impl Product {
    pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        products::table.find(id).first(&connection)
    }
}
```

`src/handlers/products.rs`:

```rust

use crate::models::product::Product;

pub fn show(id: web::Path<i32>) -> Result<HttpResponse, HttpResponse> {
    Product::find(&id)
        .map(|product| HttpResponse::Ok().json(product))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
```

`src/main.rs`:

```rust
    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to_async(handlers::products::show))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();
```

```bash
cargo run
```

If everything works you should see a shoe in http://127.0.0.1:8088/products/1

## Delete a Product

Add a new method to the Product model:

`src/models/product.rs`:

```rust
impl Product {
    pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        products::table.find(id).first(&connection)
    }

    pub fn destroy(id: &i32) -> Result<(), diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::schema::products::dsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        // Take a look at the question mark at the end, 
        // it's a syntax sugar that allows you to match 
        // the return type to the one in the method signature return, 
        // as long as it is the same error type, it works for Result and Option.
        diesel::delete(dsl::products.find(id)).execute(&connection)?;
        Ok(())
    }
}
```

`src/handlers/products.rs`:

```rust

pub fn destroy(id: web::Path<i32>) -> Result<HttpResponse, HttpResponse> {
    Product::destroy(&id)
        .map(|_| HttpResponse::Ok().json(()))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
```

`src/main.rs`:

```rust
    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to_async(handlers::products::show))
                .route(web::delete().to_async(handlers::products::destroy))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();
```

```bash
cargo run
```

Let's delete a shoe:

```bash
curl -X DELETE http://127.0.0.1:8088/products/1 \
        -H "Content-Type: application/json"

```

You should not see a shoe in http://127.0.0.1:8088/products

## Update a Product

Add the AsChangeset trait to NewProduct, this way you can pass the struct to the update directly, otherwise you need to specify every field you want to update.

`src/models/product.rs`:

```rust
#[derive(Insertable, Deserialize, AsChangeset)]
#[table_name="products"]
pub struct NewProduct {
    pub name: Option<String>,
    pub stock: Option<f64>,
    pub price: Option<i32>
}

impl Product {
    pub fn find(id: &i32) -> Result<Product, diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        products::table.find(id).first(&connection)
    }

    pub fn destroy(id: &i32) -> Result<(), diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::schema::products::dsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        diesel::delete(dsl::products.find(id)).execute(&connection)?;
        Ok(())
    }

    pub fn update(id: &i32, new_product: &NewProduct) -> Result<(), diesel::result::Error> {
        use diesel::QueryDsl;
        use diesel::RunQueryDsl;
        use crate::schema::products::dsl;
        use crate::db_connection::establish_connection;

        let connection = establish_connection();

        diesel::update(dsl::products.find(id))
            .set(new_product)
            .execute(&connection)?;
        Ok(())
    }
}
```

`src/handlers/product.rs`:

```rust
pub fn update(id: web::Path<i32>, new_product: web::Json<NewProduct>) -> Result<HttpResponse, HttpResponse> {
    Product::update(&id, &new_product)
        .map(|_| HttpResponse::Ok().json(()))
        .map_err(|e| {
            HttpResponse::InternalServerError().json(e.to_string())
        })
}
```

`src/main.rs`:

```rust
    HttpServer::new(
    || App::new()
        .service(
            web::resource("/products")
                .route(web::get().to_async(handlers::products::index))
                .route(web::post().to_async(handlers::products::create))
        )
        .service(
            web::resource("/products/{id}")
                .route(web::get().to_async(handlers::products::show))
                .route(web::delete().to_async(handlers::products::destroy))
                .route(web::patch().to_async(handlers::products::update))
        )
    )
    .bind("127.0.0.1:8088").unwrap()
    .start();
```

```bash
cargo run
```

Now, let's add stock to a product:

```rust
curl -X PATCH http://127.0.0.1:8088/products/3 \
        -H "Content-Type: application/json" \
        -d '{"stock": 8}'
```

You should now have 8 socks: http://127.0.0.1:8088/products/3.

Take a look at full source code [here](https://github.com/werner/practical-rust-web/tree/v1).

Rust is not the easiest programming language, but the benefits overcome the issues, Rust allows you to write performant and efficient applications for the long term.