Creating a REST API in Rust with warp
Tags: rust
Published: 2020-05-18 13:05:07

---
title: Creating a REST API in Rust with warp
published: true
date: 2020-04-27 13:00:03 UTC
tags: rust
canonical_url: https://blog.logrocket.com/creating-a-rest-api-in-rust-with-warp/
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/al4nrb29z0av5d7rd9n9.png
---
**Written by [Bastian Gruber](https://blog.logrocket.com/author/bastiangruber/)**✏️

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](https://github.com/seanmonstar/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.

[![LogRocket Free Trial Banner](https://i0.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/f760c-1gpjapknnuyhu8esa3z0jga.png?resize=1200%2C280&ssl=1)](https://logrocket.com/signup/)

## 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.

```jsx
cargo new neat-api --bin
```

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

```jsx
…
[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`.

```jsx
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.

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:

```jsx
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.

```jsx
use warp::{http, Filter};
```

The method for the POST request looks like this:

```jsx
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.

```jsx
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.

```jsx
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.

```jsx
#[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:

```jsx
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](https://doc.rust-lang.org/std/collections/struct.HashMap.html) 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:

```jsx
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`

```jsx
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`

```jsx
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`

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

### `DELETE`

```jsx
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](https://github.com/gruberb/warp-examaple-api). Feel free to clone and experiment and improve upon it.

* * * 
## Plug: [LogRocket](https://logrocket.com/signup/), a DVR for web apps
 
![LogRocket Dashboard Free Trial Banner](https://i2.wp.com/blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png?resize=1200%2C677&ssl=1)
 
[LogRocket](https://logrocket.com/signup/) is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
 
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
 
[Try it for free](https://logrocket.com/signup/).
* * *

The post [Creating a REST API in Rust with warp](https://blog.logrocket.com/creating-a-rest-api-in-rust-with-warp/) appeared first on [LogRocket Blog](https://blog.logrocket.com).