loading...

Practical Rust Web Development - API Rest

werner profile image Werner Echezuría Updated on ・10 min read

Practical Rust Web Development (14 Part Series)

1) Practical Rust Web Development - API Rest 2) Practical Rust Web Development - Connection Pool 3 ... 12 3) Practical Rust Web Development - Authentication 4) Practical Rust Web Development - Testing 5) Practical Rust Web Development - Cors 6) Practical Rust Web Development - Front-End 7) Practical Rust Web Development - Searching 8) Practical Rust Web Development - Pagination 9) Practical Rust Web Development - Associations 10) Practical Rust Web Development - Macros 11) Practical Rust Web Development - CI Travis 12) Practical Rust Web Development - GraphQL 13) Practical Rust Web Development - State Machine 14) Migrate to Actix-Web 2.0

Update: According to this issue 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 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

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:

[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:

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, we add a dependency in Cargo.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:

$ 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.

Let's create a table to handle products:

diesel migration generate create_products

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

up.sql:

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

down.sql:

DROP TABLE products

Applying the migration:

diesel migration run

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

src/main.rs:

#[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:

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:

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:

pub mod product;

src/models/product.rs:

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:

// 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:

serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"

main.rs:

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:

#[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:

pub mod products;

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

src/handlers/products.rs:

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:

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:

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:

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:

#[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:

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:

    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();
cargo run

We can create a new product:

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:

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:


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:

    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();
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:

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:


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:

    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();
cargo run

Let's delete a shoe:

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:

#[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:

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:

    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();
cargo run

Now, let's add stock to a product:

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.

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.

Practical Rust Web Development (14 Part Series)

1) Practical Rust Web Development - API Rest 2) Practical Rust Web Development - Connection Pool 3 ... 12 3) Practical Rust Web Development - Authentication 4) Practical Rust Web Development - Testing 5) Practical Rust Web Development - Cors 6) Practical Rust Web Development - Front-End 7) Practical Rust Web Development - Searching 8) Practical Rust Web Development - Pagination 9) Practical Rust Web Development - Associations 10) Practical Rust Web Development - Macros 11) Practical Rust Web Development - CI Travis 12) Practical Rust Web Development - GraphQL 13) Practical Rust Web Development - State Machine 14) Migrate to Actix-Web 2.0

Posted on May 14 '19 by:

werner profile

Werner Echezuría

@werner

Ruby on Rails developer and Rust enthusiast.

Discussion

markdown guide
 

This is a really awesome write-up, thank you!

Have you attempted to port an application from actix_web 0.7 to 1.0 yet? If so, how did it go? There are some significant API changes so I haven't tried yet.

 

Hi, thanks.

In the meantime I'm learning about the migration from 0.7 to 1.0 and I'm trying to document everything I can. Something I've realized it's that is easier to create handlers and assigning it to a Server.

 

Hi, your tutorial is awesome!

But I find a little issue of diesel that the line

use crate::schema::products::dsl::*;

in ProductList implementation cannot be placed in the top which will cause an unmatched type error of id in find method of Product implementation.

This issue caused by a duplicated id from crate::schema::products::dsl.

 

Hi, did you take a look at the last version, sometimes I fix these issues but I forgot to mention them, github.com/practical-rust-web-deve...

 

Thanks for your reply. Your source code can works well.

It is my bad to take the line above to the top of the module. 😄

 

you don't have models.rs? in ur project so it confuse for newcomer :) prnt.sc/p0bfqb

 

Oops!, thanks for pointing this out, fixed now.

 

thanks also like to know so you know about the swift back end? I am a little bit confuse is Rust is ready for the production type of API app for a mobile app? i have my own API written in swift that's working good but i am also interested in RUST so i start to reading you post :)

I think as an API backend Rust is ready for production, might need to polish some areas but you could have your app deployed with confident. for example: figma.com/blog/rust-in-production-...

do you know swift back end like vapor ? if yes what you think about swift back end?
thanks

To be honest, I don't know swift, so I can't make an informed opinion about it.

ok no worry thanks for reply also check this prnt.sc/p0et3l image you don't have still extern crate futures; and you add it so its give us error too

  --> src/main.rs:16:1
   |
16 | extern crate futures;
   | ^^^^^^^^^^^^^^^^^^^^^ can't find crate

also, i follow your tutorial till here Finally goes to http://localhost:8088/products.
but and trying to understand why it not showing :( my products list already debugging more then 2 hours :(

Hi, sorry for the late response, I was on vacations. Try to follow these tips and tell me if it works.

  1. git checkout v1.1.
  2. diesel setup.
  3. cargo run.
  4. curl http://127.0.0.1:8088/products -H "Content-Type: application/json" -d '{"name": "socks", "stock": 7, "price": 2}
  5. curl http://localhost:8088/products

Tell me if it works.

 

I am still going through these and am new to Rust. But being used to frameworks like flask in python, is it easy or possible to abstract out the model methods to not have the boilerplate for the db connection?

some frameworks make it so you can do request.db or maybe move it to a parent model or something? that way the model methods are a lot cleaner?

 

Well, I think it's possible, passing the db connection through a Trait, and then implements the trait for every model. Seems like a fun thing to do, maybe in a future post I'll try to implement it.

 
 

Thats a neat tutorial! Would like to see how we can use tokio in conjunction with actix. For eg. making multiple async calls(futures) to db, wait for all the futures, aggregate the resultset and respond to the request. I couldn't find any examples like that anywhere

 
impl ProductList {
    pub fn list() -> Self {
        ProductList(result)
    }
}```

 from 

to


```Rust
impl Product {
    pub fn list() -> Self {
        ProductList(result)
    }
}```



then use `Product::list()` look better and don't repeat list two times 
 

Big thanks bro....
I'm new at rust and after i read this article my mind has blow.... hha

 

No problem, I'm glad I could help.