DEV Community

Werner Echezuría
Werner Echezuría

Posted on • Updated on

Practical Rust Web Development - API Rest

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"
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

down.sql:

DROP TABLE products
Enter fullscreen mode Exit fullscreen mode

Applying the migration:

diesel migration run
Enter fullscreen mode Exit fullscreen mode

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

src/main.rs:

#[macro_use]
extern crate diesel;
extern crate dotenv;
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

src/models/mod.rs:

pub mod product;
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

src/models/product.rs:

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

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

pub mod products;
Enter fullscreen mode Exit fullscreen mode

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())
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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);"
Enter fullscreen mode Exit fullscreen mode

Then execute:

cargo run
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
        })
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

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}'

Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
        })
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

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(())
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
        })
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

Let's delete a shoe:

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

Enter fullscreen mode Exit fullscreen mode

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(())
    }
}
Enter fullscreen mode Exit fullscreen mode

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())
        })
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode
cargo run
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (23)

Collapse
 
deciduously profile image
Ben Lovy

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.

Collapse
 
werner profile image
Werner Echezuría

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.

Collapse
 
saroar profile image
Saroar Khandoker

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

Collapse
 
werner profile image
Werner Echezuría

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

Collapse
 
saroar profile image
Saroar Khandoker

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

Thread Thread
 
werner profile image
Werner Echezuría

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

Thread Thread
 
saroar profile image
Saroar Khandoker

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

Thread Thread
 
werner profile image
Werner Echezuría

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

Thread Thread
 
saroar profile image
Saroar Khandoker

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

Thread Thread
 
saroar profile image
Saroar Khandoker

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

Thread Thread
 
saroar profile image
Saroar Khandoker

here is my code please check me if you will have
bitbucket.org/Alifdev/ruststore

Thread Thread
 
werner profile image
Werner Echezuría

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.

Thread Thread
 
saroar profile image
Saroar Khandoker

yes it is working thanks :)

Collapse
 
jtr109 profile image
Ryan Li • Edited

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.

Collapse
 
werner profile image
Werner Echezuría

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

Collapse
 
jtr109 profile image
Ryan Li

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

Collapse
 
meghashyam6 profile image
meghashyam6

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

Collapse
 
autoferrit profile image
Shawn McElroy

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?

Collapse
 
werner profile image
Werner Echezuría

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.

Collapse
 
autoferrit profile image
Shawn McElroy

that would be great.

Collapse
 
saroar profile image
Saroar Khandoker
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 
Collapse
 
aistaqiemsy profile image
aistaqiemsy

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

Collapse
 
werner profile image
Werner Echezuría

No problem, I'm glad I could help.