14 releases (5 breaking)
new 0.11.0 | Apr 16, 2021 |
---|---|
0.10.2 | Apr 6, 2021 |
0.9.0 | Mar 18, 2021 |
0.6.4 | Mar 2, 2021 |
0.1.0 |
|
#94 in HTTP server
346 downloads per month
Used in 4 crates
(via salvo_core)
10KB
194 lines
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 implementsWriter
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. IfT
andE
implementsWriter
,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) = tokio::fs::copy(&file.path, Path::new(&dest)).await {
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) = tokio::fs::copy(&file.path, Path::new(&dest)).await {
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:
- basic_auth.rs
- compression.rs
- file_list.rs
- proxy.rs
- remote_addr.rs
- routing.rs
- size_limiter.rs
- sse_chat.rs
- sse.rs
- tls.rs
- todos.rs
- unix_socket.rs
- ws_chat.rs
- ws.rs
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
~0.8–1.3MB
~31K SLoC