Create a blazingly fast REST API in Rust (Part 1/2)

How to create a blazingly fast REST API in Rust, with zero-cost abstraction and very low overhead - Part 1/2

Fast, reliable, productive - Pick three | Rust's slogan

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety. Coupled with Actix, I should be able to build a fast REST API elegantly.

The idea behind this article is to see how performant a Rust API can be. I am going to create an API that saves and reads data from/to a PostgreSQL database.

This article is separate in two parts, in this first part you will learn how to:

  • Create a blazingly fast REST API in Rust
  • Connect it to a PostgreSQL database

In the second part, we will compare the performance of our application to a Go application.

Twitter clone

Twitter is a "microblogging" system that allows people to send and receive short posts called tweets.

Let's create a small part of the Twitter API to be able to post, read, and like tweets. The goal is to be able to use our Twitter clone with a massive number of simultaneous fake users.

API design

Our REST API needs to have three endpoints :

  • /tweets
    • GET: list last 50 tweets
    • POST: create a new tweet
  • /tweets/:id
    • GET: find a tweet by its ID
    • DELETE: delete a tweet by its ID
  • /tweets/:id/likes
    • GET: list all likes attached to a tweet
    • POST: add +1 like to a tweet
    • DELETE: add -1 like to a tweet

Implementation

Even though implementing an HTTP server could be fun, I choose to use Actix, which is ranked as the most performant framework ever by Techempower.

Actix Web

Actix is an actor framework prevalent in the Rust ecosystem. I am using it as an HTTP server to build our REST API.

Let's code

Three files structured our application.

  • main.rs to route HTTP requests to the right endpoint
  • tweet.rs to handle requests on /tweets
  • like.rs to handle requests on /tweets/:id/likes
main.rs
#[actix_rt::main]
async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix_web=debug,actix_server=info");
env_logger::init();
HttpServer::new(|| {
App::new()
// enable logger - always register actix-web Logger middleware last
.wrap(middleware::Logger::default())
// register HTTP requests handlers
.service(tweet::list)
.service(tweet::get)
.service(tweet::create)
.service(tweet::delete)
.service(like::list)
.service(like::plus_one)
.service(like::minus_one)
})
.bind("0.0.0.0:9090")?
.run()
.await
}

main.rs source code

With only these three files, our application is ready to receive HTTP requests. In a couple of lines, we have a fully operational application. Actix takes care of the low level boilerplate for us.

Annotation
#[get("/tweets")]

Annotation is a very convenient way to bind a route to the right path.

Validation

Let's run our application:

Run our application
# Go inside the root project directory
$ cd twitter-clone-rust
# Run the application
$ cargo run

And validate that each endpoint with no errors:

Curl commands to test our API
# list tweets
curl http://localhost:9090/tweets
# get a tweet (return status code: 204 because there is no tweet)
curl http://localhost:9090/tweets/abc
# create a tweet
curl -X POST -d '{"message": "This is a tweet"}' -H "Content-type: application/json" http://localhost:9090/tweets
# delete a tweet (return status code: 204 in any case)
curl -X DELETE http://localhost:9090/tweets/abc
# list likes from a tweet
curl http://localhost:9090/tweets/abc/likes
# add one like to a tweet
curl -X POST http://localhost:9090/tweets/abc/likes
# remove one like to a tweet
curl -X DELETE http://localhost:9090/tweets/abc/likes

At this stage, our application works without any database. Let's go more in-depth and connect it to PostgreSQL.

PostgreSQL

Diesel

Diesel is the most popular ORM in Rust to connect to a PostgreSQL database. Combined with Actix, it's a perfect fit to persist in our data. Let's see how we can make that happen. However, Diesel does not support tokio (the asynchronous engine behind Actix), so we have to run it in separate threads using the web::block function, which offloads blocking code (like Diesel's) to do not block the server's thread.

schema.rs
table! {
likes (id) {
id -> Uuid,
created_at -> Timestamp,
tweet_id -> Uuid,
}
}
table! {
tweets (id) {
id -> Uuid,
created_at -> Timestamp,
message -> Text,
}
}
joinable!(likes -> tweets (tweet_id));
allow_tables_to_appear_in_same_query!(
likes,
tweets,
);

Diesel uses a macro table!... and an internal DSL to declare the structure of our tables. There is no magic here. The code is compiled and statically linked at the compilation.

main.rs
#[actix_rt::main]
async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix_web=debug,actix_server=info");
env_logger::init();
// set up database connection pool
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL");
let manager = ConnectionManager::<PgConnection>::new(database_url);
let pool = r2d2::Pool::builder()
.build(manager)
.expect("Failed to create pool");
HttpServer::new(move || {
App::new()
// Set up DB pool to be used with web::Data<Pool> extractor
.data(pool.clone())
// enable logger - always register actix-web Logger middleware last
.wrap(middleware::Logger::default())
// register HTTP requests handlers
.service(tweet::list)
.service(tweet::get)
.service(tweet::create)
.service(tweet::delete)
.service(like::list)
.service(like::plus_one)
.service(like::minus_one)
})
.bind("0.0.0.0:9090")?
.run()
.await
}

main.rs source code

Deployment

Qovery is going to help you to deploy your application in a few seconds. Let's deploy our Twitter Clone now.

  • Sign in to the Qovery web interface.

    Qovery Sign-up page

  • Deploying the app

    1. Create a new project

      Migrate from Heroku

    2. Create a new environment

      Migrate from Heroku

    3. Create a new application

      To follow the guide, you can fork and use our repository

      Use the forked repository (and branch master) while creating the application in the repository field:

      Migrate from Heroku

    4. After the application is created:

      • Navigate application settings
      • Select Port
      • Add port 9090

      Microservices

    5. Deploy a database

      Create and deploy a new database

      To learn how to do it, you can follow this guide

    6. Configure the connection to the database

      In application overview, open the Variables tab

      Open Variable

      Configure the alias for each built_in environment variable to match the one required within your code

      Env Var Alias

      Have a look at this section to know more on how to connect to a database.

    7. Deploy your application

      All you have to do now is to navigate to your application and click Deploy button

      Deploy App

      That's it. Watch the status and wait till the app is deployed.

    Congratulations, you have deployed your application!

    Live test

    To open the application in your browser, click on Action and Open buttons in your application overview:

    Open App

    Then, we can test it with the following CURL commands (replace the app URL with your own):

    Curl commands to test our deployed API
    # create a tweet
    curl -X POST -d '{"message": "This is a tweet"}' -H "Content-type: application/json" https://main-gxbuagyvgnkbrp5l-gtw.qovery.io/tweets
    # list tweets
    curl https://main-gxbuagyvgnkbrp5l-gtw.qovery.io/tweets
    # get a tweet
    curl https://main-gxbuagyvgnkbrp5l-gtw.qovery.io/tweets/<change_with_a_valid_id>
    # list likes from a tweet
    curl https://main-gxbuagyvgnkbrp5l-gtw.qovery.io/tweets/<change_with_a_valid_id>/likes
    # add one like to a tweet
    curl -X POST https://main-gxbuagyvgnkbrp5l-gtw.qovery.io/tweets/<change_with_a_valid_id>/likes
    # remove one like to a tweet
    curl -X DELETE https://main-gxbuagyvgnkbrp5l-gtw.qovery.io/tweets/<change_with_a_valid_id>/likes
    # delete a tweet
    curl -X DELETE https://main-gxbuagyvgnkbrp5l-gtw.qovery.io/tweets/<change_with_a_valid_id>

    What's next

    In this first part we saw how to create a Rust API with Actix and Diesel. In the second part we will compare its performance with a Go application to see which one is the most performant.

    Special thanks to Jason and Kokou for your reviews

    Useful resources

    Do you want to know more about Rust?

    Tutorial