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.

Web interface

Sign up with Github or Gitlab to the Qovery web interface.

Qovery Sign-up page

Permissions

Qovery needs to get access to your Github account to deploy the application.

Click here to give access!

Qovery needs two files at the root of your project to deploy an application:

  • .qovery.yml to declare the dependencies that your application need (E.g PostgreSQL).
  • Dockerfile to build and run your application.
.qovery.yml
application:
name: twitter-clone-rust
project: Twitter-Clone
publicly_accessible: true
databases:
- type: postgresql
version: "11.5"
name: my-postgresql-8628210
routers:
- name: main
routes:
- application_name: twitter-clone-rust
paths:
- /

Now, commit and push your .qovery.yml and Dockerfile:

Git commit and push
$ git add .qovery.yml Dockerfile
$ git commit -m "add .qovery.yml Dockerfile"
$ git push -u origin master
Create DATABASE_URL env var
$ qovery project env add DATABASE_URL '$QOVERY_DATABASE_MY_POSTGRESQL_8628210_CONNECTION_URI'

Congratulations, you have deployed your application.

Live test

To test our deployed API, execute the following command to find your API root URL:

Get our API root URL
$ qovery status
Output
BRANCH NAME | STATUS | ENDPOINTS | APPLICATIONS | DATABASES
master | running | https://main-gxbuagyvgnkbrp5l-gtw.qovery.io | twitter-clone-rust | my-postgresql-8628210
APPLICATION NAME | STATUS | DATABASES
twitter-clone-rust | running | my-postgresql-8628210
DATABASE NAME | STATUS | TYPE | VERSION | ENDPOINT | PORT | USERNAME | PASSWORD | APPLICATIONS
my-postgresql-8628210 | running | POSTGRESQL | 11.5 | <hidden> | <hidden> | <hidden> | <hidden> | twitter-clone-rust

Then, we can test it with the following CURL commands:

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