To Master This Art

Why Build This Framework

As a beginner, I found myself struggling to grasp existing frameworks like actix-web and Rocket. When I attempted to rewrite a previous Go web service in Rust, each framework seemed more complex than those available in Go. Given Rust's already steep learning curve, why complicate web frameworks further?

When Tokio introduced the Axum framework, I was thrilled, thinking I could finally stop maintaining my own web framework. However, I soon realized that while Axum appeared simple, it required extensive type gymnastics and generic definitions. Crafting even a basic middleware demanded deep Rust expertise and writing copious amounts of obscure boilerplate code.

Thus, I decided to continue maintaining my unique web framework—one that is intuitive, feature-rich, and beginner-friendly.

Is Salvo Right for You?

Salvo is simple yet comprehensive and powerful, arguably the strongest in the Rust ecosystem. Despite its capabilities, it remains easy to learn and use, sparing you any "self-castration" pains.

  • Ideal for Rust beginners: CRUD operations are commonplace, and with Salvo, such tasks feel as straightforward as with frameworks in other languages (e.g., Express, Koa, Gin, Flask). In some aspects, Salvo is even more abstract and concise.

  • Suitable for production: If you aim to deploy Rust in production for robust, high-speed servers, Salvo fits the bill. Although not yet at version 1.0, its core features have undergone years of iteration, ensuring stability and timely issue resolution.

  • Perfect for you, especially if your hair is thinning and shedding daily.

Achieving Simplicity

Hyper handles many low-level implementations, making it a reliable foundation for general needs. Salvo follows this approach, offering a powerful and flexible routing system alongside essential features like Acme, OpenAPI, and JWT authentication.

Salvo unifies Handlers and Middleware: Middleware is a Handler. Both are attached to the Router via hoop. Essentially, they process Request and may write data to Response. A Handler receives three parameters: Request, Depot (for temporary data during request processing), and Response.

For convenience, you can omit unnecessary parameters or ignore their order.

use salvo::prelude::*;

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

The routing API is exceptionally simple yet powerful. For typical use cases, you only need to focus on the Router type.

Additionally, if a struct implements relevant traits, Salvo can automatically generate OpenAPI documentation, extract parameters, handle errors gracefully, and return user-friendly messages. This makes writing handlers as intuitive as writing ordinary functions. We'll explore these features in detail later. Here’s an example:

#[endpoint(tags("message_logs"))]
pub async fn create_message_log_handler(
    input: JsonBody<CreateOrUpdateMessageLog>,
    depot: &mut Depot,
) -> APPResult<Json<MessageLog>> {
    let db = utils::get_db(depot)?;
    let log = create_message_log(&input, db).await?;
    Ok(Json(log))
}

In this example, JsonBody<CreateOrUpdateMessageLog> automatically parses JSON from the request body into the CreateOrUpdateMessageLog type (supporting multiple data sources and nested types). The #[endpoint] macro generates OpenAPI documentation for this endpoint, simplifying parameter extraction and error handling.

Routing System

I believe Salvo’s routing system stands out. Routers can be flat or tree-like, distinguishing between business logic trees and access directory trees. The business logic tree organizes routers based on business needs, which may not align with the access directory tree.

Typically, routes are written 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 listing them don’t require user login, but creating, editing, or deleting articles does. Salvo’s nested routing system elegantly handles this. We can group public routes together:

Router::new()
    .path("articles")
    .get(list_articles)
    .push(Router::new().path("{id}").get(show_article));

Then, group protected routes with middleware for authentication:

Router::new()
    .path("articles")
    .hoop(auth_check)
    .post(create_article)
    .push(Router::new().path("{id}").patch(edit_article).delete(delete_article));

Even though both routers share the same path("articles"), they can be added to the same parent router, resulting in:

Router::new()
    .push(
        Router::new()
            .path("articles")
            .get(list_articles)
            .push(Router::new().path("{id}").get(show_article)),
    )
    .push(
        Router::new()
            .path("articles")
            .hoop(auth_check)
            .post(create_article)
            .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)),
    );

{id} matches a path segment. Typically, an article id is numeric, so we can restrict matching with a regex: r"{id:/\d+/}".