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 Go web service in Rust, every framework seemed more complex than those in Go. Rust already has a steep learning curve—why make web frameworks even more convoluted?

When Tokio released Axum, I was thrilled, thinking I could finally stop maintaining my own web framework. However, in practice, Axum's apparent simplicity masked excessive type gymnastics and generic definitions. Writing even a simple middleware required deep Rust expertise and verbose, obscure boilerplate code.

Thus, I decided to continue maintaining my own framework—one that's intuitive, feature-rich, and beginner-friendly.

Is Salvo Right for You?

Salvo is simple yet powerful, arguably the most capable in the Rust ecosystem. Despite its strength, it remains easy to learn and use, sparing you the pain of unnecessary complexity.

  • For Rust beginners: CRUD operations are commonplace, and with Salvo, they feel as straightforward as in frameworks like Express, Koa, Gin, or Flask—sometimes even more abstract and concise.
  • For production use: Though not yet at version 1.0, Salvo's core features have been refined over years of iteration, ensuring stability and timely fixes for robust, high-performance servers.
  • For the thinning-haired developer: A framework that won’t add to your hair loss.

How Salvo Achieves Simplicity

Hyper handles much of the low-level work, making it a solid foundation. Salvo builds on this with a powerful, flexible routing system and essential features like Acme, OpenAPI, and JWT Auth.

Salvo unifies Handlers and Middleware: Middleware is a Handler, attached via hoop to a Router. Both process Request and may write to Response. Handlers take three parameters:

  • Request: The incoming request.
  • Depot: Temporary data storage during processing.
  • Response: The outgoing response.

For brevity, unused parameters can be omitted or reordered:

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 equally simple yet powerful. Typically, you only need to focus on the Router type.

Salvo also automates OpenAPI documentation, parameter extraction, and error handling. Writing handlers feels as natural as writing plain functions. 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))
}

Here, JsonBody<CreateOrUpdateMessageLog> automatically parses JSON (supporting nested types and multi-source data), while #[endpoint] generates OpenAPI docs and streamlines error handling.

The Routing System

Salvo’s routing stands apart. Routers can be flat or nested into trees, decoupling business logic trees from URL paths.

A typical setup:

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

Public routes (e.g., listing/viewing articles) and private routes (e.g., editing/deleting) can be grouped separately, with middleware enforcing auth:

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

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

Both can coexist under a parent router, forming this structure:

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. To restrict it to digits, use a regex: r"{id:/\d+/}".