32 releases (9 breaking)

new 0.10.4 Apr 9, 2021
0.9.6 Apr 1, 2021
0.9.5 Mar 30, 2021
0.3.1 Dec 27, 2020
0.1.6 Feb 23, 2020

#4 in #salvo

Download history 46/week @ 2020-12-21 34/week @ 2020-12-28 34/week @ 2021-01-04 113/week @ 2021-01-11 165/week @ 2021-01-18 35/week @ 2021-01-25 51/week @ 2021-02-01 44/week @ 2021-02-08 108/week @ 2021-02-15 49/week @ 2021-02-22 159/week @ 2021-03-01 162/week @ 2021-03-08 107/week @ 2021-03-15 152/week @ 2021-03-22 233/week @ 2021-03-29 274/week @ 2021-04-05

522 downloads per month
Used in 3 crates (2 directly)

MIT/Apache

260KB
6K SLoC

Savlo

build status build status build status
codecov crates.io Download License

Salvo is a web server framework written in Rust.

🎯 Features

  • Base on hyper, tokio and async supported;
  • Websocket supported;
  • Middleware is handler and support executed before or after handle;
  • Easy to use routing system, routers can be nested, and you can add middleware in routers;
  • multipart form supported, handle files upload is very simple;
  • Serve a static virtual directory from many physical directories;

⚡️ Quick start

You can view samples here or read docs here.

Create a new rust project:

cargo new hello_salvo --bin

Add this to Cargo.toml

[dependencies]
salvo = "0.10"
tokio = { version = "1", features = ["full"] }

Create a simple function handler in the main.rs file, we call it hello_world, this function just render plain text "Hello World".

use salvo::prelude::*;

#[fn_handler]
async fn hello_world(_req: &mut Request, _depot: &mut Depot, res: &mut Response) {
    res.render_plain_text("Hello World");
}

There are many ways to write function handler.

  • You can omit function arguments if they do not used, like _req, _depot in this example:

    #[fn_handler]
    async fn hello_world(res: &mut Response) {
        res.render_plain_text("Hello World");
    }
    
  • Any type can be function handler's return value if it implements Writer. For example &str implements Writer and it will render string as plain text:

    #[fn_handler]
    async fn hello_world(res: &mut Response) -> &'static str {// just return &str
        "Hello World"
    }
    
  • The more common situation is we want to return a Result<T, E> to implify error handling. If T and E implements Writer, Result<T, E> can be function handler's return type:

    #[fn_handler]
    async fn hello_world(res: &mut Response) -> Result<&'static str, ()> {// return Result
        Ok("Hello World")
    }
    

In the main function, we need to create a root Router first, and then create a server and call it's bind function:

use salvo::prelude::*;

#[fn_handler]
async fn hello_world() -> &'static str {
    "Hello World"
}
#[tokio::main]
async fn main() {
    let router = Router::new().get(hello_world);
    let server = Server::new(router);
    server.bind(([0, 0, 0, 0], 7878)).await;
}

Middleware

There is no difference between Handler and Middleware, Middleware is just Handler.

Tree-like routing system

Normally we write routing like this:

Router::new().path("articles").get(list_articles).post(create_article);
Router::new()
    .path("articles/<id>")
    .get(show_article)
    .patch(edit_article)
    .delete(delete_article);

Often viewing articles and article lists does not require user login, but creating, editing, deleting articles, etc. require user login authentication permissions. The tree-like routing system in Salvo can meet this demand. We can write routers without user login together:

Router::new()
    .path("articles")
    .get(list_articles)
    .push(Router::new().path("<id>").get(show_article));

Then write the routers that require the user to login together, and use the corresponding middleware to verify whether the user is logged in:

Router::new()
    .path("articles")
    .before(auth_check)
    .post(list_articles)
    .push(Router::new().path("<id>").patch(edit_article).delete(delete_article));

Although these two routes have the same path("articles"), they can still be added to the same parent route at the same time, so the final route looks like this:

Router::new()
    .push(
        Router::new()
            .path("articles")
            .get(list_articles)
            .push(Router::new().path("<id>").get(show_article)),
    )
    .push(
        Router::new()
            .path("articles")
            .before(auth_check)
            .post(list_articles)
            .push(Router::new().path("<id>").patch(edit_article).delete(delete_article)),
    );

<id> matches a fragment in the path, under normal circumstances, the article id is just a number, which we can use regular expressions to restrict id matching rules, r"<id:/\d+/>".

For numeric characters there is an easier way to use <id:num>, the specific writing is:

  • <id:num>, matches any number of numeric characters;
  • <id:num[10]>, only matches a certain number of numeric characters, where 10 means that the match only matches 10 numeric characters;
  • <id:num(..10)> means matching 1 to 9 numeric characters;
  • <id:num(3..10)> means matching 3 to 9 numeric characters;
  • <id:num(..=10)> means matching 1 to 10 numeric characters;
  • <id:num(3..=10)> means match 3 to 10 numeric characters;
  • <id:num(10..)> means to match at least 10 numeric characters.

You can also use <*> or <**> to match all remaining path fragments. In order to make the code more readable, you can also add appropriate name to make the path semantics more clear, for example: <**file_path>.

It is allowed to combine multiple expressions to match the same path segment, such as /articles/article_<id:num>/.

File upload

We can get file async by the function get_file in Request:

#[fn_handler]
async fn upload(req: &mut Request, res: &mut Response) {
    let file = req.get_file("file").await;
    if let Some(file) = file {
        let dest = format!("temp/{}", file.filename().unwrap_or_else(|| "file".into()));
        if let Err(e) = std::fs::copy(&file.path, Path::new(&dest)) {
            res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
        } else {
            res.render_plain_text("Ok");
        }
    } else {
        res.set_status_code(StatusCode::BAD_REQUEST);
    }
}

Multiple files also very simple:

#[fn_handler]
async fn upload(req: &mut Request, res: &mut Response) {
    let files = req.get_files("files").await;
    if let Some(files) = files {
        let mut msgs = Vec::with_capacity(files.len());
        for file in files {
            let dest = format!("temp/{}", file.filename().unwrap_or_else(|| "file".into()));
            if let Err(e) = std::fs::copy(&file.path, Path::new(&dest)) {
                res.set_status_code(StatusCode::INTERNAL_SERVER_ERROR);
                res.render_plain_text(&format!("file not found in request: {}", e.to_string()));
            } else {
                msgs.push(dest);
            }
        }
        res.render_plain_text(&format!("Files uploaded:\n\n{}", msgs.join("\n")));
    } else {
        res.set_status_code(StatusCode::BAD_REQUEST);
        res.render_plain_text("file not found in request");
    }
}

More Examples

Your can find more examples in examples folder:

Some code and examples port from warp, multipart-async, mime-multipart and actix-web.

☕ Supporters

Salvo is an open source project. If you want to support Salvo, you can ☕ buy a coffee here.

⚠️ License

Salvo is licensed under MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT).

Dependencies

~10–15MB
~302K SLoC