Which web framework to choose for rest & websockets

Hi,
I want to migrate two applications in rust to a web-framework for its API. I am trying to figure out which web-framework to choose and am hoping to get some feedback from people who tried to use the same features.
The first currently uses hyper directly, the second uses polling on mysql as it's "API" and is currently being upgraded from 2015-ish Rust to 2018 edition. Both boil down to nearly the same requirements for a web framework:

  • REST-like API (post/get to different paths with JSON & JSON response)
  • web-sockets to ping-back clients after work is done and broadcast to all when for example the global queue changes.
  • being able to run some blocking stuff (DB) /things in tokio and then kick off the broadcast / client ping back. (So not entirely an action-reaction system as many async-libs are having in their examples.)
  • Stable rust, can be latest

Libraries I've looked into:

  • tower-web: currently doesn't build and throws "not yet implemented" errors with its examples (trying to use extract, response)
  • warp: seems a direct competitor to tower-web, so which one to pick ?! Both seem to have some kind of stale development lately. This can be totally fine but I can't really see whether it's finished for my case or not.
  • actix: As of now the most mature. I've already written one rest-like API with it, but nothing out of the action-reaction scheme. Also I'm still concerned about it's current state regarding unsafe transmutations to &mut internally.

For websockets I've found a multitude of specific frameworks only to handle these. As I need some kind of authentification to reply to specific users for their request, getting websockets from the same framework used for the REST-API is probably better.

I'm happy to answer questions as I'm sure that I wasn't able to perfectly describe my needs.

1 Like

Disclaimer: I've only used Rocket and Warp.

I would not say warp development is stale. Github shows the last commit was 8 days ago. I have also found @seanmonstar to be exceedingly responsive to questions/issues raised on the repo. For REST functions and web-sockets, warp is likely to be as simple to get going as you could imagine. Not sure about the DB stuff you mentioned so others will need to chime in there.

I have not used tower, but rather than a competitor to warp they are co-conspirators with plans to merge the two approaches at some point. I actually hope this does not happen because I like the simplicity that warp provides.

You might be interested in gotham which has documentation implying a focus on async via tokio.

1 Like

Just to clarify: I've only seen that tower & warp are coming from the same developer team and that according to github the development stalled compared to previously. This plus my experience with tower-web made it a little unclear how much intention there is to support this in the future (and which one).
I know that @seanmonstar and @carllerche a doing a bunch of work while I'm just looking on the user-space crate tower-web/warp. I just don't want to start adopting a "dying horse". You could say I'm too lazy to switch over when that happens & I've figured out how to deal with all the async stuff in my brain.

I'll look into gotham.

tower-web development is not stalled. Most of the work on it is happening in ecosystem crates to extract what has been learned and to stage the next big iteration.

tower-web is not going anywhere, though it will become part of warp once the infrastructure is in place (which is what is being worked on).

These things take time.

1 Like

Thanks for for your work and clarifying. I'm trying to re-verify my crash with the recent updates. The recent update fixed these issues for me. Also I haven't found anything regarding tower-web and websockets, or am I missing something ?

As of now with tower-web there is no websocket support. There are ways to make it work with some effort (at the tower level).

Iā€™m unsure if websocket support will happen before or after merging with warp

I've now implemented a part in tower-web, that works well for simple, stupid APIs that don't care about sessions etc. I'll leave that part in the backend as it works flawless.
For the frontend I tried some stuff in warp as I initially wanted to use it. That turned out to be quite repetitive, the same API would've been much more work and repetitive code. So I've had to scrap that.
Now I'm looking into gotham and will see whether it'll be gotham or actix for the frontend.

Also I wanted to note that in gotham the amount of documentation in the examples is nearly overwhelming.

I don't think I understand how you're using the terms frontend and backend. warp does not attempt to be a frontend tool but a backend server that can communicate with and serve to the web your frontend. You can use template engines to create the frontend for server side rendering, which would then be provided by warp but I'm not sure that's what you meant.

While I'm aware I might have a special use case I don't think there's a clear definition and it depends on the project so here's mine for this one:
Backend: Handles some low level stuff and in this case even lives inside a docker container due to the size of dependencies. It's kinda realtime and doesn't know anything about users, permissions, permanent storage etc. Lives in its own virtual network. You could say a daemon with a REST API.
Frontend: Does the actual State handling, storage. Knows about users, login etc. Also shows a website.
Daemon/Backend <-> "Frontend" <-> Browser/RPC

Getting warp to handle a system of "/callback/:method" but with the restriction to one specific IP, some security header but a different json body to de-serialize per method required me to write the filters again for every method supported.
Edit: This is the callback-code so the daemon can call the frontend on events. I've specifically decided against using things like websockets for this (although this would be bi-directional) as it's more robust. One call can fail but nothing else is affected.

    warp::addr::remote().and_then(|addr: Option<SocketAddr>| {
        if let Some(v) = addr {
            if v == backend.addr {
                return Ok(());
            }
        }
        Err(warp::reject::custom("403 Forbidden"))
    })
    .and(warp::body::content_length_limit(1024 * 256))
    .and(warp::body::json())
    .and(warp::path(/* path for method */)
    .map(|_addr, body: /* for every method supported again..*/|

You could use this for every path again via

.and(warp::path("callback"))
    .and(warp::path::param()).map(move|param: String, body: /* now what ? */|)

but as already questioned in that example, now I have the problem of only being able to specify one type to deserialize into.

Afaik the other things just existing already in other frameworks are handling of sessions & co.

let base_path = warp::addr::remote().and_then(|addr: Option&lt;SocketAddr&gt;| { 
    if let Some(v) = addr { 
        if v == backend.addr { 
            return Ok(()); 
        }
   }
   Err(warp::reject::custom("403 Forbidden")) 
})
.and(warp::body::content_length_limit(1024 * 256)) 
.and(warp::body::json())

You can then do your_path = base_path.and(warp::path(/* your path */).map(|??| ??) for each path, so there's not quite as much redundancy. And depending on what your specific function in the map call is, you could abstract that into a closure or function. Sessions are not built into warp at present, but they can be done. I would say that at the present time warp is far and away the easiest framework to get started with for simple things, but the more polished and mature frameworks would be better fits for more complex situations like yours.

But you can't do base_path.and() more than once as it'll consume base_path per docs

Have you actually tried it? The most recent app I've been working on using warp has something similar:

let app_name = warp::path("app_name");
let app_root = app_name.and(warp::path::end()).and(warp::fs::file("path/to/my/file.html");
let login = app_name.and(warp::path("login")).and(map || /* calling my handlebars template for logging in */);
let callback = app_name.and(.and(warp::path("callback"))
            .and(warp::filters::query::raw())
            .map(|code: String| AccessCode::from_query_str(&code))
            .map(|accesscode: AccessCode| {
                println!("{}", accesscode.to_string());
                OauthToken::from_access_code(accesscode)
            })
            .map(|resp: Result<reqwest::Response>| {
                let mut unwrapped = resp.unwrap();
                println!("{:?}", unwrapped);
                unwrapped.json::<OauthToken>()
            })
            .map(|token: Result<OauthToken>| {
                println!("token: {:?}", token);
                let id_token = &token.unwrap().id_token;
                println!("{:?}", alcoholic_jwt::token_kid(id_token));
                id_token.to_string()
            })
            .map(|token: String| {
                alcoholic_jwt::validate(&token,
                                        &jwk_by_kid(auth0_jwks(),
                                                    &alcoholic_jwt::token_kid(&token)
                                                         .unwrap()
                                                         .unwrap()),
                                        vec![])
                        .unwrap()
            })
            .map(|valid_jwt: ValidJWT| format!("jwt claims: {:?}", valid_jwt.claims));

let routes = app_root.or(login).or(callback);

I've tried the following:

    let mut base = warp::post2().and(warp::addr::remote().and_then(|addr: Option<SocketAddr>| {
        if let Some(v) = addr {
            if v == backend.addr {
                return Ok(());
            }
        }
        Err(warp::reject::custom("403 Forbidden"))
    }))
    .and(warp::body::content_length_limit(1024 * 256))
    .and(warp::body::json());

    let path_1 = base.and(warp::path(callback::PATH_INSTANCE)).map(move|_valid_ip, body: callback::InstanceStateResponse|{

    });

    let path_2 = base.and(warp::path(callback::PATH_PLAYBACK)).map(move|_valid_ip, body: callback::PlaystateResponse|{

    });

/// more path_X methods to follow
path_2
expected signature of `fn((), yamba_types::models::callback::InstanceStateResponse) -> _`