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:

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.