# Salvo: a Rust web framework where middleware is just a handler

> Salvo builds on Hyper and Tokio with tree-based routing, HTTP/1, HTTP/2 and HTTP/3 support, and an #[endpoint] macro that generates OpenAPI documentation. Here is what the repository documents, what it leaves out, and which projects should stay away.

**salvo-rs/salvo** — A powerful web framework built with a simplified design.

- Repository: https://github.com/salvo-rs/salvo
- Website: https://salvo.rs
- Stars: 4,434 · Forks: 272
- Language: Rust
- License: Apache-2.0
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/salvo-rs-salvo

## What Salvo is for, and who it is aimed at

Salvo is a Rust web framework described in its README as "a powerful and simple Rust web framework". It targets engineers who already write Rust and want an HTTP server without assembling Hyper and Tokio by hand. The README's feature list names HTTP/1, HTTP/2 and HTTP/3, tree-based routing with middleware at any level, ACME certificate management, OpenAPI support, WebSocket and WebTransport, and a foundation on Hyper and Tokio. The repository is a Cargo workspace: the root Cargo.toml declares members = ["crates/*"], and the workspace dependencies list separates salvo_core, salvo_macros, salvo_extra, salvo-oapi and feature crates such as salvo-acme, salvo-compression, salvo-cache, salvo-cors, salvo-csrf, salvo-flash and salvo-jwt-auth. That split is the honest description of the project's shape. The core is small and the protocols, security helpers and integrations are opt-in crates. If you want a framework that hands you one import and hides everything, this is not it. If you want to compile only the pieces you use, the layout is designed for that. The README does not state who should not use it, so the boundary has to be read from the version number and the MSRV: the workspace version is 0.96.0 and rust-version is 1.94, which means a pre-1.0 API and a recent toolchain.

## Middleware is a handler: the routing and dispatch model

The design decision the README leads with is that middleware and handlers are the same thing. A function annotated with #[handler] can serve a request or run before one, and the difference is where you attach it. Middleware is added with hoop on a Router, and because routers form a tree, a hoop applies to the branch it is attached to rather than to the whole application. The README's example shows a public articles route and a second articles route that carries an auth_check hoop before its post and delete methods. That is the mechanism: routing and middleware share one tree, and dispatch walks it. Handlers receive typed extractors as arguments. The README shows a handler taking &mut Response to insert a header, and another taking &mut Request and returning Result<Json<Todo>, ParseError> after calling req.parse_json::<CreateTodo>().await. The #[handler] macro generates the glue. There is no separate extractor trait to implement for the common cases, no tower::Service stack to compose, and no generic type parameter list to thread through your function signatures. The cost of that simplicity is that the framework owns the dispatch shape. If your existing middleware is written against tower, the README does not describe an adapter, and the repository layout does not list one. You would be rewriting that layer as Salvo handlers.

## Installing Salvo and running a first route

The README gives the install path as Cargo commands, not a system package. Create a binary crate, add salvo and tokio with the oapi and macros features, then replace the contents of src/main.rs.

```bash
cargo new hello-salvo
cd hello-salvo
cargo add salvo tokio --features salvo/oapi,tokio/macros
```

The generated main.rs is replaced with the README's first app: a #[handler] returning a string, a Router with a GET route, a TcpListener bound to 127.0.0.1:7878, and Server::new(acceptor).serve(router).await.

```rust
use salvo::prelude::*;

#[handler]
async fn hello() -> &'static str {
    "Hello World"
}

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);
    let acceptor = TcpListener::new("127.0.0.1:7878").bind().await;
    Server::new(acceptor).serve(router).await;
}
```

Run it with cargo run and request the port. The README does not describe the startup log line, so expect output from Tokio and Salvo rather than a documented banner. If you want a JSON endpoint instead, the README's todo example needs Serde's derive feature, added with cargo add serde --features derive, and the handler reads the body through req.parse_json. There is also a scaffolding CLI: cargo install salvo-cli followed by salvo new my_project. The README does not document what the generated project contains, so treat it as a starting point to inspect rather than a known layout.

## OpenAPI by changing one attribute

Salvo's OpenAPI story is the sharpest thing in the README. You write your handler, and to document it you change #[handler] to #[endpoint]. The README's example is the same hello function with the attribute swapped, and the text says this gives OpenAPI support in one line. The workspace Cargo.toml backs that up with salvo-oapi and salvo-oapi-macros as separate crates at the same version as the rest of the workspace, and the install command in the README enables salvo/oapi. The consequence is that the documentation and the implementation cannot drift in the way they do when a schema file is maintained by hand, because the macro reads the function's signature. The limitation is equally clear: this only works for functions written in the shape the macro understands. A handler that builds a response dynamically, or returns an untyped body, gives the macro nothing to describe. The README does not document how to override the generated schema, how to add descriptions or examples, or how the generated document is served. For a small typed API the trade is good; for a large API with hand-tuned documentation, expect to read the oapi crate sources.

## TLS, HTTP/3 and the protocol crates you opt into

Automatic certificates are built into the listener rather than bolted on. The README's ACME example chains .acme(), .add_domain("example.com"), .http01_challenge(&mut router) and .quinn("0.0.0.0:443") onto TcpListener::new("0.0.0.0:443"), with the comment that quinn adds HTTP/3 support. Three example directories cover ACME variants: acme-http01, acme-tls-alpn01 and acme-http01-quinn. The workspace also lists salvo-http3 as a dependency, and note its version: 0.7.0, while every other workspace crate is 0.96.0. That mismatch is worth understanding before you depend on it, because it means the HTTP/3 crate is versioned on its own track and an upgrade of the framework does not automatically move it. The README does not document certificate storage, renewal timing, staging versus production ACME endpoints, or what happens when the challenge fails. Those are exactly the questions that decide whether automatic certificates are usable in production, and they are not answered in the README. Read the acme examples and the salvo-acme crate before pointing this at a real domain.

## Where Salvo is the wrong choice

The clearest limitation is API stability. The workspace version is 0.96.0 and the project is pre-1.0, so semver permits breaking changes in the minor position. The releases confirm the pace: v0.96.0 on 2026-08-27, v0.95.2 on 2026-08-06, v0.95.1 on 2026-07-29. Three releases in about a month is the profile of a framework still settling, and the last push was on 2026-09-21. If your organisation requires a stable interface with a long support window, this is not that, and no amount of documentation changes it. The second limitation is documentation depth. The README covers the happy path and points to the examples directory and docs.rs for everything else. There is no documented rollback procedure, no documented graceful shutdown sequence, no documented request body size limit, and no documented error page contract in the README; the repository does contain examples/catch-error, examples/catch-panic, examples/custom-error-page and examples/concurrency-limiter, which tells you the answers exist but live in code. The third is the ecosystem. Salvo is not tower-based, so middleware written for Axum or Actix Web does not drop in. Budget for rewriting that layer.

## Axum and Actix Web: the actual difference

The natural comparison is Axum, because both build on Hyper and Tokio and both are Rust HTTP frameworks. The difference is in the composition model. Axum routes through tower::Service, so middleware is a Service that wraps another Service, and the tower ecosystem is available to you. Salvo routes through its own tree, and middleware is a #[handler] attached with hoop at a node in that tree. The practical result: in Axum you can reuse tower middleware and you pay for it with generic type signatures; in Salvo the signatures stay flat and you write the middleware yourself. A second difference is scope. Salvo ships ACME, OpenAPI, WebSocket, WebTransport, CSRF, JWT and flash as workspace crates, and the README advertises HTTP/3 through quinn. Axum's README is not in front of me, so I will not characterise its feature set; the honest statement is that Salvo's own README claims these as built-in, and the workspace Cargo.toml lists the crates that provide them. Actix Web is the other name engineers reach for, and it has its own actor-derived runtime history, which is a different architecture again. Pick Salvo when tree-scoped middleware and the built-in protocol crates match your problem. Pick Axum when reusing tower middleware matters more than flat signatures.

## Licence, upgrades and what maintenance looks like

Salvo is licensed under Apache-2.0, stated in the README and in the workspace Cargo.toml license field. Apache-2.0 is a permissive licence with an explicit patent grant, and the practical consequence for adopters is that you can ship it in a closed product provided you keep the licence notice and the NOTICE file if one exists; I am not a lawyer and the repository's LICENSE file is the authority, not this paragraph. The workspace also carries a SECURITY.md and a CONTRIBUTING.md at the top level, so there is a stated process for both. On upgrades, the versioning is the risk. Every workspace crate is pinned to the same version except salvo-http3, which sits at 0.7.0, so a framework bump does not imply an HTTP/3 bump. The repository has a CHANGELOG.md at the top level, and given three releases in roughly a month, reading it before each bump is the difference between a routine upgrade and an afternoon of compile errors. The minimum supported Rust version is 1.94 and the edition is 2024, so an upgrade of Salvo can force a toolchain upgrade. The README does not document a deprecation policy or a support window for older releases.

## Conclusion

Adopt Salvo if you are building a Rust HTTP service that needs tree-scoped middleware, HTTP/3, or OpenAPI generated from the same functions that serve traffic, and you are willing to read the examples directory because the README documents the happy path only. Do not adopt it if you need a stable API surface: the workspace version is 0.96.0, the MSRV is 1.94, and the edition is 2024, so pin the version and read CHANGELOG.md before every upgrade. Before committing, verify the behaviour you depend on against the examples that ship with the version you install, because graceful shutdown, error handling and body limits are demonstrated there rather than described in the README.

## FAQ

### How do I install Salvo?

The README uses Cargo: create a crate with cargo new hello-salvo, then run cargo add salvo tokio --features salvo/oapi,tokio/macros. There is also a scaffolding CLI, installed with cargo install salvo-cli and run as salvo new my_project.

### What is Salvo, the Rust project?

It is a Rust web framework described in its README as a powerful and simple Rust web framework, built on Hyper and Tokio. Its feature list names HTTP/1, HTTP/2 and HTTP/3, tree-based routing with middleware, ACME certificates, OpenAPI, WebSocket and WebTransport.

### Does Salvo support HTTP/3?

The README's ACME example chains .quinn("0.0.0.0:443") onto the listener with the comment that this adds HTTP/3 support, and the workspace lists salvo-http3 as a dependency at version 0.7.0. Note that this crate's version does not match the 0.96.0 used by the rest of the workspace.

### How does Salvo generate OpenAPI documentation?

By changing the attribute on a handler from #[handler] to #[endpoint]. The README states this gives OpenAPI support in one line, and the salvo-oapi and salvo-oapi-macros crates are separate workspace members at the same version as the framework.

### What Rust version does Salvo require?

The workspace Cargo.toml sets rust-version = "1.94" and edition = "2024", and the README's badge states Rust 1.94 or later. Upgrading Salvo can therefore require upgrading your toolchain.

### Can I reuse tower middleware with Salvo?

The README does not describe a tower adapter. Salvo middleware is a function annotated with #[handler] and attached with hoop at a node in the router tree, so middleware written for tower-based frameworks would need to be rewritten as Salvo handlers.

## Sources

- [License: Apache-2.0](https://github.com/salvo-rs/salvo/blob/main/LICENSE)
- [Project website](https://salvo.rs)
- [README](https://github.com/salvo-rs/salvo/blob/main/README.md)
- [Releases](https://github.com/salvo-rs/salvo/releases)
- [salvo-rs/salvo on GitHub](https://github.com/salvo-rs/salvo)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/salvo-rs-salvo
