API organization in Rust

Hey, Rust Community. I found a great start guide for building some basic endpoints but now I am trying to figure out how to parse out the struct, service, repo and main.rs. I am a total beginner with rust and coming from the OOP world. Can anybody point me in some good directions for learning api dev and maybe organizing this api I am starting? I am very excited about the possibility of writing API's using rust. Here is my starter code:

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_inventory(
    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 inventory",
        http::StatusCode::CREATED,
    ))
}

async fn change_inventory(
    item: Item,
    store: Store
) -> Result<impl warp::Reply, warp::Rejection> {
    let zero = 0;
    let value = store.grocery_list.write().get(&item.name).unwrap_or(&zero) + item.quantity;
    store.grocery_list.write().insert(item.name, value);

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

async fn delete_inventory_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 inventory",
        http::StatusCode::OK,
    ))
}

async fn get_inventory(
    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_inventory);

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

    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_inventory_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(change_inventory);

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

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

Good organization of a project and APIs is a broad topic, so it's hard to say it all in a post.

Rust has some guidelines. They're more for libraries, but you may find some helpful tips there:

For smaller things, run cargo clippy.

In your case: I'd use Arc<Store> rather than Arc inside, so that it's clear that Store is shared by the endpoints, rather than copied each time.

IIRC warp has some helper methods so that you don't need to write the full path for every endpoint definition.

Rust's iterators have collect() method (via FromIterator trait) that avoids insert in a loop. You could probably just dereference the existing list. I presume you've copied it because of a type error caused by read() wrapper, but you can use * operator to get the &HashMap out of it.

Hi!
@kornel referred most of it, and if you want to check some popular projects using warp check dim or the newly released Signal Calling Service for example.
I also have a pet project to showcase some of warp patterns, and if you have questions feel free to come to the #warp channel on the tokio discord.

Cheers :slight_smile:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.