Async Web Dev Using Actix-Web and Postgres

Darin Gordon

Rust NYC Meetup

January 16, 2020

Text

Async Rust

So. Much. Better!

Readability

chained combinators

async-await

Async Rust

So. Much. Better!

Productivity

  .fold(Vec::<i64>::new(), move |mut things, thing| {
        let clone_everything = everything.clone();
                      
        call_something_async(...)
        .and_then(move |item| 
             call_more_things(....)
             .and_then(move |thing| please_kill_me()))
                

aggregating using combinators

let mut things = Vec::new::<i64>();

for item in get_items.await? {
    let result = do_async_stuff(item).await?;
    
    things.push(result);
}

sigh_in_relief(result).await?;

aggregating using plain loops

Async Rust

So. Much. Better!

  • Easier to use
  • Easier to understand
  • More productive

.. But most importantly:

One popular async use case?

Web Dev.

Web Dev

(Backend)

Most popular frameworks

(.. by downloads)

Framework All-time downloads 7-day avg downloads Async?
Actix-Web 812,897 2,200 Native
Rocket 385,377 800 In Progress
Warp 120,857 550 Native

source: crates.io  -  Jan 15, 2020

Actix-Web

  • std futures
  • async-await
  • updated examples

v2.0 released at end of 2019

  • Top-ranking contender in TechEmpower Benchmarks
  • Used in major production environments (Azure IoT)
  • Mature architecture (many iterations)
  • Dedicated leadership
  • Documentation, Examples, and Ecosystem
  • Rich in Features
  • Community

Actix-Web

Concerns?

Uses Unsafe (responsibly)

Actix-Web

Code for the talk:

https://bit.ly/

2szEACk

This is a Lightning Talk (a quick tour)

A simple resource-creation REST endpoint that uses an async postgres workflow

  • async-await
  • See actix-web in Action
  • Tokio-Postgres
  • Pool (deadpool)
  • Data Mapping

Goals

Actix-Web

Define our REST endpoint

"Add a User to the system"

POST /users

body = {"username": ...,"first_name": ..., "last_name": ..., "email": ...}

Modeling

body = {"username": ...,"first_name": ..., "last_name": ..., "email": ...}

mod models {
    use serde::{Deserialize, Serialize};
    use tokio_pg_mapper_derive::PostgresMapper;

    #[derive(Deserialize, PostgresMapper, Serialize)]
    #[pg_mapper(table = "users")] // singular 'user' is a keyword..
    pub struct User {
        pub email: String,
        pub first_name: String,
        pub last_name: String,
        pub username: String
    }
}

Error Handling

mod errors {
	use actix_web::{HttpResponse, ResponseError};
	use derive_more::{Display, From};
	use deadpool_postgres::PoolError;
	use tokio_postgres::error::Error as PGError;
	use tokio_pg_mapper::Error as PGMError;


	#[derive(Display, From, Debug)]
	pub enum MyError {
		NotFound,
		PGError(PGError),
		PGMError(PGMError),
		PoolError(PoolError)
	}
	impl std::error::Error for MyError {}

	impl ResponseError for MyError {
		fn error_response(&self) -> HttpResponse {
			match *self {
				MyError::NotFound => HttpResponse::NotFound().finish(),
				_ => HttpResponse::InternalServerError().finish()
			}	
		}
	}
}

Database Logic

mod db {
	use crate::{errors::MyError, models::User};
	use deadpool_postgres::Client;
	use tokio_pg_mapper::FromTokioPostgresRow;


	pub async fn add_user(client: &Client, user_info: User) -> Result<User, MyError> {
		let _stmt = include_str!("../sql/add_user.sql");
		let _stmt = _stmt.replace("$table_fields", &User::sql_table_fields());
		let stmt = client.prepare(&_stmt)
						 .await
						 .unwrap();

		client.query(&stmt, [
                    &user_info.email,
                    &user_info.first_name,
                    &user_info.last_name,
                    &user_info.username])
               .await?
               .iter()
               .map(|row| User::from_row_ref(row).unwrap())
               .collect::<Vec<User>>()
               .pop()
               .ok_or(MyError::NotFound) // more applicable for SELECTs
	}
}

Web Handlers

mod handlers {
	use actix_web::{HttpResponse, web, Error};
	use deadpool_postgres::{Client, Pool};
        use crate::{db, errors::MyError, models::User};


	pub async fn add_user(user: web::Json<User>, db_pool: web::Data<Pool>)
						-> Result<HttpResponse, Error> {

		let user_info: User = user.into_inner();

		let client: Client =
                    db_pool.get()
                    .await
                    .map_err(|err| MyError::PoolError(err))?;

		let new_user = db::add_user(&client, user_info).await?;
		
		Ok(HttpResponse::Ok().json(new_user))
	}
}
use actix_web::{App, HttpServer, web};
use deadpool_postgres::{Pool, Manager};
use handlers::add_user;
use tokio_postgres::{Config, NoTls};

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    const SERVER_ADDR: &str = "127.0.0.1:8080";

	let pg_config = "postgres://test_user:testing@127.0.0.1:5432/testing_db"
					.parse::<Config>()
					.unwrap();

	let pool = Pool::new(
        Manager::new(pg_config, NoTls),
		16 // # of connections in pool
    );

    let server = HttpServer::new(move ||
			App::new()
				.data(pool.clone())
				.service(web::resource("/users").route(web::post().to(add_user)))
		)
        .bind(SERVER_ADDR)?
        .run();
    println!("Server running at http://{}/", SERVER_ADDR);

    server.await
}

Main

... and more!

  • Middleware

  • Testing API
  • Extractor pattern
  • Highly configurable
  • ...

Questions?

Code for the talk:

https://bit.ly/

2szEACk

Copy of deck

By Darin Gordon

Copy of deck

  • 436