DEV Community

Alex Spinov
Alex Spinov

Posted on

Salvo Has a Free API: A Rust Web Framework With Built-in OpenAPI and Acme TLS

Salvo is a Rust web framework focused on simplicity and productivity. It features automatic OpenAPI documentation, built-in ACME (Let's Encrypt) TLS, and a middleware system that makes Rust web development feel effortless.

Why Salvo Matters

Axum is powerful but requires assembling many pieces. Salvo includes OpenAPI generation, TLS certificates, WebSocket support, and rate limiting out of the box — with less boilerplate.

What you get for free:

  • Automatic OpenAPI 3.0 documentation generation
  • Built-in ACME TLS (automatic Let's Encrypt certificates)
  • WebSocket support
  • Rate limiting middleware
  • CORS, logging, compression built in
  • Proxy and static file serving
  • Less boilerplate than Axum for common tasks

Quick Start

cargo new my-api
cd my-api

# Add to Cargo.toml:
# salvo = { version = "0.70", features = ["oapi"] }
# tokio = { version = "1", features = ["full"] }

cargo run
Enter fullscreen mode Exit fullscreen mode

Hello World

use salvo::prelude::*;

#[handler]
async fn hello(res: &mut Response) {
    res.render(Text::Plain("Hello from Salvo!"));
}

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);
    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}
Enter fullscreen mode Exit fullscreen mode

REST API with OpenAPI

use salvo::prelude::*;
use salvo::oapi::extract::*;

#[derive(Serialize, Deserialize, ToSchema)]
struct User {
    id: u64,
    name: String,
    email: String,
}

#[derive(Deserialize, ToSchema)]
struct CreateUser {
    name: String,
    email: String,
}

#[endpoint(
    tags("Users"),
    summary = "List all users",
    responses(
        (status_code = 200, description = "Success", body = Vec<User>)
    )
)]
async fn list_users(res: &mut Response) {
    let users = vec![
        User { id: 1, name: "Alice".into(), email: "alice@example.com".into() },
        User { id: 2, name: "Bob".into(), email: "bob@example.com".into() },
    ];
    res.render(Json(users));
}

#[endpoint(
    tags("Users"),
    summary = "Create a user",
)]
async fn create_user(
    body: JsonBody<CreateUser>,
    res: &mut Response,
) {
    let user = User {
        id: 3,
        name: body.name.clone(),
        email: body.email.clone(),
    };
    res.render(Json(user));
}

#[tokio::main]
async fn main() {
    let router = Router::new()
        .push(Router::with_path("users")
            .get(list_users)
            .post(create_user)
        );

    // Auto-generate OpenAPI docs at /api-doc
    let doc = OpenApi::new("My API", "1.0.0").merge_router(&router);
    let router = router
        .push(doc.into_router("/api-doc/openapi.json"))
        .push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}
Enter fullscreen mode Exit fullscreen mode

Automatic HTTPS (ACME/Let's Encrypt)

use salvo::prelude::*;
use salvo::conn::rustls_acme::AcmeListener;

#[tokio::main]
async fn main() {
    let router = Router::new().get(hello);

    // Automatic TLS certificate from Let's Encrypt!
    let acceptor = AcmeListener::builder()
        .domain("myapp.example.com")
        .cache_path("./certs")
        .bind("0.0.0.0:443")
        .await;

    Server::new(acceptor).serve(router).await;
}
Enter fullscreen mode Exit fullscreen mode

Middleware

use salvo::prelude::*;
use salvo::rate_limiter::*;
use salvo::cors::Cors;
use salvo::logging::Logger;
use salvo::compression::Compression;

#[tokio::main]
async fn main() {
    let cors = Cors::new()
        .allow_origin("https://myapp.com")
        .allow_methods(vec!["GET", "POST"])
        .into_handler();

    let limiter = RateLimiter::new(
        FixedGuard::new(),
        MokaStore::new(),
        RemoteIpIssuer,
        BasicQuota::per_second(10),  // 10 req/sec per IP
    );

    let router = Router::new()
        .hoop(Logger::new())
        .hoop(Compression::new().min_length(1024))
        .hoop(cors)
        .hoop(limiter)
        .get(hello);

    let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
    Server::new(acceptor).serve(router).await;
}
Enter fullscreen mode Exit fullscreen mode

Useful Links


Building Rust web services? Check out my developer tools on Apify for ready-made web scrapers, or email spinov001@gmail.com for custom solutions.

Top comments (0)