Bastian Gruber I'm a passionate software developer who mainly works with Rust to create services for the web. Next to that, I write for several tech magazines and publish articles on my own. Every now and then I get interviewed as well.

Creating a REST API in Rust with warp

7 min read 2233

Creating a REST API in Rust with warp

Rust is a lot of folks’ favorite programming language, but it can still be hard to find a project for it or even to get a firm grasp of it. A good way to get started with any language is to build something you will use every day. If your company operates microservices, it’s even easier. Rust is well-suited to replace such a service, and you could rewrite it in a matter of days.

When you first get started with Rust, you’ll need to learn the fundamentals. Once you’re familiar with the syntax and basic concepts, you can start thinking about asynchronous Rust. Most modern languages have a build in runtime that handles async tasks, such as sending off a request or waiting in the background for an answer.

In Rust, you have to choose a runtime that works for you. Libraries usually have their own runtime; if you work on a larger project, you may want to avoid adding multiple runtimes.

Tokio is the most production-used and proven runtime that can handle asynchronous tasks, so chances are high that your future employer already uses it. Your choices are therefore somewhat limited since you may need to choose a library that already has Tokio built in to create your API.

For this tutorial, we’ll use warp. Depending on your previous programming experience, it may take a few days to wrap your head around it. But once you understand warp, it can be quite an elegant tool for building APIs.

Setting up your project

To follow along with this tutorial, you’ll need to install the following libraries.

  • warp for creating the API
  • Tokio to run an asynchronous server
  • Serde to help serialize incoming JSON
  • parking_lot to create a ReadWriteLock for your local storage

First, create a new project with cargo.

cargo new neat-api --bin

We’ve included warp in our Cargo.toml so we can use it throughout our codebase.

…
[dependencies]
warp = "0.2"
parking_lot = "0.10.0"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "0.2", features = ["macros"] }

For a first test, create a simple “Hello, World!” in main.rs.

use warp::Filter;

#[tokio::main]
async fn main() {
    // GET /hello/warp => 200 OK with body "Hello, warp!"
    let hello = warp::path!("hello" / String)
        .map(|name| format!("Hello, {}!", name));

    warp::serve(hello)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

Filters are a way to parse a request and match against a route we created. So when you start the server via cargo run and point your browser to localhost:3030/hello/WHATEVER, warp sends this request through its filters and executes the first one that is triggered.

We made a custom demo for .
No really. Click here to check it out.

In let hello = … we created a new path, essentially saying that every request with the path /hello plus a string gets handled by this method. So, we return Hello, WHATEVER.

If we point the browser to localhost:3030/hello/new/WHATEVER, we’ll get a 404 since we don’t have a filter for /hello/new + String.

Building the API

Let’s build a real API to demonstrate these concepts. A good model is an API for a grocery list. We want to be able to add items to the list, update the quantity, delete items, and view the whole list. Therefore, we need four different routes with the HTTP methods GET, DELETE, PUT, and POST.

With so many different routes, is it wise to create methods for each instead of handling them all in main.rs.

Creating local storage

In addition to routes, we need to store a state in a file or local variable. In an async environment, we have to make sure only one method at a time can access the store so there are no inconsistencies between threads. In Rust, we have Arc so the compiler knows when to drop a value and a read and write lock (RwLock). That way, no two methods on different threads are writing to the same memory.

Your store implementation should look like this:

use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;

type Items = HashMap<String, i32>;

#[derive(Debug, Deserialize, Serialize, Clone)]
struct Item {
    name: String,
    quantity: i32,
}

#[derive(Clone)]
struct Store {
  grocery_list: Arc<RwLock<Items>>
}

impl Store {
    fn new() -> Self {
        Store {
            grocery_list: Arc::new(RwLock::new(HashMap::new())),
        }
    }
}

POSTing an item to the list

Now we can add our first route. To add items to the list, make an HTTP POST request to a path. Our method has to return a proper HTTP code so the caller knows whether their call was successful. warp offers basic types via its own http library, which we need to include as well.

use warp::{http, Filter};

The method for the POST request looks like this:

async fn add_grocery_list_item(
    item: Item,
    store: Store
    ) -> Result<impl warp::Reply, warp::Rejection> {
        store.grocery_list.write().insert(item.name, item.quantity);


        Ok(warp::reply::with_status(
            "Added items to the grocery list",
            http::StatusCode::CREATED,
        ))
}

The warp framework offers the option to “reply with status,” so we can add text plus a generic HTTP status so the caller knows whether the request was successful or if they have to try again.

Now add a new route and call the method you just created for it. Since you can expect a JSON for this, you should create a little json_body helper function to extract the Item out of the body of the HTTP request.

In addition, we need to pass the store down to each method by cloning it and creating a warp filter, which we call in the .and() during the warp path creation.

fn json_body() -> impl Filter<Extract = (Item,), Error = warp::Rejection> + Clone {
    // When accepting a body, we want a JSON body
    // (and to reject huge payloads)...
    warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}

#[tokio::main]
async fn main() {
    let store = Store::new();
    let store_filter = warp::any().map(move || store.clone());

    let add_items = warp::post()
        .and(warp::path("v1"))
        .and(warp::path("groceries"))
        .and(warp::path::end())
        .and(json_body())
        .and(store_filter.clone())
        .and_then(add_grocery_list_item);

    warp::serve(add_items)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

You can test the POST call via curl or an application such as Postman, which is now a standalone application for making HTTP requests. Start the server via cargo run and open another terminal window or tab to execute the following curl.

curl --location --request POST 'localhost:3030/v1/groceries' \
--header 'Content-Type: application/json' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "apple",
    "quantity": 3
}'

You should get the text response and HTTP code as defined in your method.

GETting the grocery list

Now we can post a list of items to our grocery list, but we still can’t retrieve them. We need to create another route for the GET request. Our main function will add this new route. For this new route, we don’t need to parse any JSON.

#[tokio::main]
async fn main() {
    let store = Store::new();
    let store_filter = warp::any().map(move || store.clone());

    let add_items = warp::post()
        .and(warp::path("v1"))
        .and(warp::path("groceries"))
        .and(warp::path::end())
        .and(json_body())
        .and(store_filter.clone())
        .and_then(add_grocery_list_item);

    let get_items = warp::get()
        .and(warp::path("v1"))
        .and(warp::path("groceries"))
        .and(warp::path::end())
        .and(store_filter.clone())
        .and_then(get_grocery_list);


    let routes = add_items.or(get_items);

    warp::serve(routes)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

You’ll get a taste of async Rust when you examine the data structure behind your Arc. You’ll need to .read() and then .iter() over the data inside the RwLock, so create a new variable to return to the caller. Due to the nature of Rust’s ownership model, you can’t simply read and return the underlying list of groceries. Here is the method:

async fn get_grocery_list(
    store: Store
    ) -> Result<impl warp::Reply, warp::Rejection> {
        let mut result = HashMap::new();
        let r = store.grocery_list.read();


        for (key,value) in r.iter() {
            result.insert(key, value);
        }

        Ok(warp::reply::json(
            &result
        ))
}

Create a variable for the store.grocery_list.read(). Then, iterate over the HashMap and write every key/value pair into a new one, which you’ll return via warp::reply::json().

UPDATE and DELETE

The last two missing methods are UPDATE and DELETE. For DELETE, you can almost copy your add_grocery_list_item, but instead of .insert(), .remove() an entry.

A special case is the update. Here the Rust HashMap implementation uses .insert() as well, but it updates the value instead of creating a new entry if the key doesn’t exist.

Therefore, just rename the method and call it for the POST as well as the PUT.

For the DELETE method, you need to pass just the name of the item, so create a new struct and add another parse_json() method for the new type. Rename the first parsing method and add another one.

You can simply rename your add_grocery_list_item method to call it update_grocery_list and call it for a warp::post() and warp::put(). Your complete code should look like this:

use warp::{http, Filter};
use parking_lot::RwLock;
use std::collections::HashMap;
use std::sync::Arc;
use serde::{Serialize, Deserialize};

type Items = HashMap<String, i32>;

#[derive(Debug, Deserialize, Serialize, Clone)]
struct Id {
    name: String,
}

#[derive(Debug, Deserialize, Serialize, Clone)]
struct Item {
    name: String,
    quantity: i32,
}

#[derive(Clone)]
struct Store {
  grocery_list: Arc<RwLock<Items>>
}

impl Store {
    fn new() -> Self {
        Store {
            grocery_list: Arc::new(RwLock::new(HashMap::new())),
        }
    }
}

async fn update_grocery_list(
    item: Item,
    store: Store
    ) -> Result<impl warp::Reply, warp::Rejection> {
        store.grocery_list.write().insert(item.name, item.quantity);


        Ok(warp::reply::with_status(
            "Added items to the grocery list",
            http::StatusCode::CREATED,
        ))
}

async fn delete_grocery_list_item(
    id: Id,
    store: Store
    ) -> Result<impl warp::Reply, warp::Rejection> {
        store.grocery_list.write().remove(&id.name);


        Ok(warp::reply::with_status(
            "Removed item from grocery list",
            http::StatusCode::OK,
        ))
}

async fn get_grocery_list(
    store: Store
    ) -> Result<impl warp::Reply, warp::Rejection> {
        let mut result = HashMap::new();
        let r = store.grocery_list.read();


        for (key,value) in r.iter() {
            result.insert(key, value);
        }

        Ok(warp::reply::json(
            &result
        ))
}

fn delete_json() -> impl Filter<Extract = (Id,), Error = warp::Rejection> + Clone {
    // When accepting a body, we want a JSON body
    // (and to reject huge payloads)...
    warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}

fn post_json() -> impl Filter<Extract = (Item,), Error = warp::Rejection> + Clone {
    // When accepting a body, we want a JSON body
    // (and to reject huge payloads)...
    warp::body::content_length_limit(1024 * 16).and(warp::body::json())
}

#[tokio::main]
async fn main() {
    let store = Store::new();
    let store_filter = warp::any().map(move || store.clone());

    let add_items = warp::post()
        .and(warp::path("v1"))
        .and(warp::path("groceries"))
        .and(warp::path::end())
        .and(post_json())
        .and(store_filter.clone())
        .and_then(update_grocery_list);

    let get_items = warp::get()
        .and(warp::path("v1"))
        .and(warp::path("groceries"))
        .and(warp::path::end())
        .and(store_filter.clone())
        .and_then(get_grocery_list);

    let delete_item = warp::delete()
        .and(warp::path("v1"))
        .and(warp::path("groceries"))
        .and(warp::path::end())
        .and(delete_json())
        .and(store_filter.clone())
        .and_then(delete_grocery_list_item);


    let update_item = warp::put()
        .and(warp::path("v1"))
        .and(warp::path("groceries"))
        .and(warp::path::end())
        .and(post_json())
        .and(store_filter.clone())
        .and_then(update_grocery_list);



    let routes = add_items.or(get_items).or(delete_item).or(update_item);

    warp::serve(routes)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

Testing curls

After you update the code, restart the server via cargo run and use these curls to post, update, get, and delete items.

POST

curl --location --request POST 'localhost:3030/v1/groceries' \
--header 'Content-Type: application/json' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "apple",
    "quantity": 3
}'

UPDATE

curl --location --request PUT 'localhost:3030/v1/groceries' \
--header 'Content-Type: application/json' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "apple",
    "quantity": 5
}'

GET

curl --location --request GET 'localhost:3030/v1/groceries' \
--header 'Content-Type: application/json' \
--header 'Content-Type: text/plain'

DELETE

curl --location --request DELETE 'localhost:3030/v1/groceries' \
--header 'Content-Type: application/json' \
--header 'Content-Type: text/plain' \
--data-raw '{
    "name": "apple"
}'

Final thoughts

The code is far from perfect. For example, we could optimize the return message as well as the return HTTP codes. We would need to implement proper error handling in case we pass the wrong JSON format to the server. The code would also need to be tested.

To summarize the steps we just covered:

  • Create an ID for each item so you can update and delete via /v1/groceries/{id}
  • Add a 404 route
  • Add error handling for malformatted JSON
  • Adjust the return messages for each route
  • Add tests for each route

You can see how straightforward it is to create your first REST API with Rust and warp and how the Rust type system makes clear what data you’re handling and what methods are available to you. Now it’s up to you to hone your skills and optimize the code.

You can find the full source code on GitHub. Feel free to clone and experiment and improve upon it.

You come here a lot! We hope you enjoy the LogRocket blog. Could you fill out a survey about what you want us to write about?

    Which of these topics are you most interested in?
    ReactVueAngularNew frameworks
    Do you spend a lot of time reproducing errors in your apps?
    YesNo
    Which, if any, do you think would help you reproduce errors more effectively?
    A solution to see exactly what a user did to trigger an errorProactive monitoring which automatically surfaces issuesHaving a support team triage issues more efficiently
    Thanks! Interested to hear how LogRocket can improve your bug fixing processes? Leave your email:

    LogRocket: Full visibility into production Rust apps

    Debugging Rust applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking performance of your Rust apps, automatically surfacing errors, and tracking slow network requests and load time, try LogRocket.

    LogRocket is like a DVR for web apps, recording literally everything that happens on your Rust app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.

    Modernize how you debug your Rust apps — .

    Bastian Gruber I'm a passionate software developer who mainly works with Rust to create services for the web. Next to that, I write for several tech magazines and publish articles on my own. Every now and then I get interviewed as well.

    3 Replies to “Creating a REST API in Rust with warp”

    1. Thanks, I was stuck but your tutorial helped me. I’m noob to rust so I would love more tutorials 🙂

    2. Thanks for the tut. One remark though:
      “Due to the nature of Rust’s ownership model, you can’t simply read and return the underlying list of groceries.”
      That’s not true, the Deref trait on Mutex allows us to do this:

      Ok(warp::reply::json(&*store.grocery_list.read()));

      Cheers 🙂

    Leave a Reply