A sample project that uses Warp and Tokio to build a simple asynchronous API.

This tutorial introduces the Warp framework by building an asynchronous CRUD API. I wrote this with the following goals in mind:

  1. Become familiar with the Warp framework.
  2. Become more familiar with using async/await in Rust
  3. Get a better understanding of API design in Rust

Design

Before getting into any coding, I’ll sketch out the design of the API. This will help with determining the necessary endpoints, handler functions, and how to store data.

Routes

For this API, I only need two routes.

/customers 
    - GET -> list all customers in data store - POST -> create new customer and insert into data store 
/customers/{guid} 
    - GET -> list info for a customer 
    - POST -> update information for a customer 
    - DELETE -> remove customer from data store

Handlers

Based on the defined routes, I will need the following handlers:

list_customers -> return a list all customers in database create_customer -> create a new customer and add it to the database get_customer -> return the details of a single customer update_customer -> update the details of a single customer delete_customer -> delete a customer from the database

Database

For right now, I’ll just use an in-memory data store to share across the route handlers.

I used Mockaroo to generate a JSON data set of customer data. The data is a JSON array where each object has the following structure:

{
    "guid": "String",
    "first_name": "String",
    "last_name": "String",
    "email": "String",
    "address": "String"
}

Also, the database module will need to have the ability to initialize the data store once the server starts.

Dependencies

As of right now, I know that I will need the following dependencies:

  • Warp — A web server framework for Rust
  • Tokio — An asynchronous run-time for Rust
  • Serde — A de/serialization library for converting JSON to typed data and vice versa.

Implementation

Models

The first thing I want to do is define my customer model and also start adding some structure to the code.

In main.rs, define a new module called models like this:

mod models;

fn main() {
    // ...
}

Then create a new file called models.rs and add the following:

pub struct Customer {
    pub guid: String,
    pub first_name: String,
    pub last_name: String,
    pub email: String,
    pub address: String,
}

Since I’m designing an API, this data structure needs be able to covert to and from JSON. I also want to be able to copy the structure into and out of the data store without having to worry about the borrow checker.

To accomplish this, I’ll add a derive statement to use a couple of the macros from the Serde library and a couple from Rust. Now models.rs looks like this:

Image for post

Database

The database for this example API will be an in-memory database that is a vector of the Customer model. However, the data store will need to be shared across multiple routes, so we can use one of Rust’s smart pointer along with a Mutex to allow for thread safety.

First, update main.rs with a new module called db:

mod db;
mod models;

fn main() {
    // ...
}

Then create a new file called db.rs.

There are a few things to do in this file, but the first thing to do is to define what the data store will look like.

A simple data store is just a vector of Customer structs, but it needs to be wrapped in a thread safe reference to be able to use multiple references of the data store in multiple asynchronous handlers.

Add the following to db.rs:

use std::sync::Arc;
use tokio::sync::Mutex;

use crate::models::Customer;

pub type Db = Arc<Mutex<Vec<Customer>>>;

Now that we have defined the structure of the data store, we need a way to initialize the data store. Initializing the data store has two outcomes, either an empty data store or a data store loaded with data from a data file.

An empty store is rather straight forward.

pub fn init_db() -> Db {
    Arc::new(Mutex::new(Vec::new()))
}

But in order to load data from a file, we need to add another dependency:

Add the following to the Cargo.toml file:

serde_json = "1.0"

Now we can update db.rs with the following:

use std::fs::File;
use serde_json::from_reader;

pub fn init_db() -> Db {
    let file = File::open("./data/customers.json");
    match file => {
        Ok(json) => {
            let customers = from_reader(json).unwrap();
            Arc::new(Mutex::new(customers))
        },
        Err(_) => {
            Arc::new(Mutex::new(Vec::new()))
        }
    }
}

This function attempts to read from the file at ./data/customers.json. If it is successful, the function returns a data store loaded with the customer data, else it returns an empty vector.

The db.rs should look like this now:

Image for post

#rustlang #rust #api-development #api #toki #warp

Building an API using Toki and Warp
1.55 GEEK