| name | salvo-openapi |
| description | Generate OpenAPI documentation automatically from Salvo handlers. Use for API documentation, Swagger UI, and API client generation. |
| version | 0.89.3 |
| tags | ["advanced","openapi","swagger","documentation"] |
Salvo OpenAPI Integration
This skill helps generate OpenAPI 3.0 documentation from Salvo applications.
Key Difference: #[handler] vs #[endpoint]
#[handler] - Basic Salvo handler, no OpenAPI documentation
#[endpoint] - Generates OpenAPI documentation automatically
Use #[endpoint] for all handlers that should appear in API documentation.
Setup
Add dependencies:
[dependencies]
salvo = { version = "0.89.3", features = ["oapi"] }
serde = { version = "1", features = ["derive"] }
Basic Usage
Use #[endpoint] instead of #[handler]:
use salvo::oapi::extract::*;
use salvo::prelude::*;
#[endpoint]
async fn hello(name: QueryParam<String, false>) -> String {
format!("Hello, {}!", name.as_deref().unwrap_or("World"))
}
#[tokio::main]
async fn main() {
let router = Router::new().push(Router::with_path("hello").get(hello));
let doc = OpenApi::new("My API", "1.0.0").merge_router(&router);
let router = router
.unshift(doc.into_router("/api-doc/openapi.json"))
.unshift(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;
}
OpenAPI Extractors
These extractors work with both #[handler] and #[endpoint], but only generate documentation with #[endpoint]:
Path Parameters
use salvo::oapi::extract::PathParam;
#[endpoint]
async fn get_user(id: PathParam<i64>) -> String {
format!("User ID: {}", id.into_inner())
}
Query Parameters
重要:必须使用 QueryParam 提取器,禁止使用 req.query() 手动解析查询参数。req.query() 不会生成 OpenAPI 文档,使用 QueryParam 才能自动生成参数说明。
use salvo::oapi::extract::QueryParam;
#[endpoint]
async fn search(q: QueryParam<String, false>) -> String {
format!("Search: {}", q.as_deref().unwrap_or(""))
}
#[endpoint]
async fn search_required(q: QueryParam<String, true>) -> String {
format!("Search: {}", q.into_inner())
}
JSON Body
use salvo::oapi::extract::JsonBody;
#[endpoint]
async fn create_user(user: JsonBody<CreateUser>) -> StatusCode {
StatusCode::CREATED
}
Request Body Documentation
use salvo::oapi::ToSchema;
use serde::{Deserialize, Serialize};
#[derive(Deserialize, ToSchema)]
struct CreateUser {
name: String,
#[salvo(schema(example = "user@example.com"))]
email: String,
}
#[endpoint]
async fn create_user(body: JsonBody<CreateUser>) -> StatusCode {
StatusCode::CREATED
}
Response Documentation
use salvo::oapi::ToSchema;
use serde::Serialize;
#[derive(Serialize, ToSchema)]
struct User {
id: i64,
name: String,
email: String,
}
#[endpoint]
async fn get_user(id: PathParam<i64>) -> Json<User> {
Json(User {
id: id.into_inner(),
name: "John".to_string(),
email: "john@example.com".to_string(),
})
}
Query Parameters with ToParameters
use salvo::oapi::{ToParameters, ToSchema};
use serde::Deserialize;
#[derive(Deserialize, ToParameters)]
struct Pagination {
#[salvo(parameter(default = 1, minimum = 1))]
page: Option<u32>,
#[salvo(parameter(default = 20, minimum = 1, maximum = 100))]
per_page: Option<u32>,
}
#[endpoint]
async fn list_users(pagination: Pagination) -> Json<Vec<User>> {
let page = pagination.page.unwrap_or(1);
let per_page = pagination.per_page.unwrap_or(20);
Json(vec![])
}
Status Codes and Error Responses
use salvo::oapi::ToSchema;
use serde::Serialize;
#[derive(Serialize, ToSchema)]
struct ErrorResponse {
message: String,
}
#[endpoint(
status_codes(200, 404),
responses(
(status_code = 200, description = "User found", body = User),
(status_code = 404, description = "User not found", body = ErrorResponse),
)
)]
async fn get_user(id: PathParam<i64>) -> Result<Json<User>, StatusError> {
Ok(Json(User {
id: id.into_inner(),
name: "John".to_string(),
email: "john@example.com".to_string(),
}))
}
Tags, Summary and Description
#[endpoint(
tags("users"),
summary = "Create a new user",
description = "Creates a new user account with the provided information"
)]
async fn create_user(body: JsonBody<CreateUser>) -> StatusCode {
StatusCode::CREATED
}
OpenAPI Document Generation
use salvo::oapi::{OpenApi, Info, License};
#[tokio::main]
async fn main() {
let router = Router::new()
.push(Router::with_path("users").get(list_users).post(create_user))
.push(Router::with_path("users/{id}").get(show_user));
let doc = OpenApi::new("My API", "1.0.0")
.info(
Info::new("My API", "1.0.0")
.description("API description")
.license(License::new("MIT"))
)
.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;
}
Swagger UI
use salvo::oapi::swagger_ui::SwaggerUi;
let router = router.push(
SwaggerUi::new("/api-doc/openapi.json")
.into_router("/swagger-ui")
);
Schema Customization
use salvo::oapi::ToSchema;
use serde::Serialize;
#[derive(Serialize, ToSchema)]
#[salvo(schema(example = json!({"id": 1, "name": "John", "email": "john@example.com"})))]
struct User {
id: i64,
#[salvo(schema(minimum = 1, maximum = 100))]
age: Option<u8>,
#[salvo(schema(pattern = "^[a-zA-Z]+$"))]
name: String,
#[salvo(schema(format = "email"))]
email: String,
}
Security Schemes
use salvo::oapi::security::{Http, HttpAuthScheme, SecurityScheme};
use salvo::prelude::*;
#[tokio::main]
async fn main() {
let router = Router::new()
.push(Router::with_path("users").get(list_users))
.push(Router::with_path("profile").get(get_profile));
let doc = OpenApi::new("My API", "1.0.0")
.description("API with authentication")
.add_security_scheme(
"bearer_auth",
SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer))
)
.merge_router(&router);
let router = router
.unshift(doc.into_router("/api-doc/openapi.json"))
.unshift(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;
}
#[endpoint(
security(("bearer_auth" = []))
)]
async fn get_profile() -> &'static str {
"Protected profile"
}
File Upload Documentation
use salvo::oapi::extract::*;
#[endpoint(
tags("files"),
request_body(content = "multipart/form-data")
)]
async fn upload_file(req: &mut Request) -> Result<Json<UploadResponse>, StatusError> {
let file = req.file("file").await
.ok_or_else(|| StatusError::bad_request())?;
let filename = file.name().unwrap_or("unnamed").to_string();
let size = file.size();
Ok(Json(UploadResponse { filename, size }))
}
Complete OpenAPI Setup Example
use salvo::oapi::extract::*;
use salvo::oapi::{OpenApi, ToSchema, ToParameters};
use salvo::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, ToSchema)]
struct User {
id: i64,
name: String,
email: String,
}
#[derive(Deserialize, ToSchema)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Deserialize, ToParameters)]
struct Pagination {
#[salvo(parameter(default = 1))]
page: Option<u32>,
#[salvo(parameter(default = 20))]
per_page: Option<u32>,
}
#[endpoint(tags("users"), summary = "List all users")]
async fn list_users(pagination: Pagination) -> Json<Vec<User>> {
Json(vec![])
}
#[endpoint(tags("users"), summary = "Get user by ID")]
async fn get_user(id: PathParam<i64>) -> Result<Json<User>, StatusError> {
Ok(Json(User {
id: id.into_inner(),
name: "John".to_string(),
email: "john@example.com".to_string(),
}))
}
#[endpoint(tags("users"), summary = "Create a new user", status_codes(201))]
async fn create_user(body: JsonBody<CreateUser>) -> StatusCode {
StatusCode::CREATED
}
#[tokio::main]
async fn main() {
let router = Router::new()
.push(
Router::with_path("users")
.get(list_users)
.post(create_user)
.push(Router::with_path("{id}").get(get_user))
);
let doc = OpenApi::new("User API", "1.0.0")
.description("A comprehensive user management API")
.contact_name("API Support")
.contact_email("support@example.com")
.license_name("MIT")
.merge_router(&router);
let router = router
.unshift(doc.into_router("/api-doc/openapi.json"))
.unshift(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;
}
Best Practices
- Use
#[endpoint] for documented handlers: All API handlers should use #[endpoint]
- Derive ToSchema for all types: Request/response types need
ToSchema
- Add meaningful descriptions: Use doc comments and attributes
- Group endpoints with tags: Organize API documentation
- Document error responses: Include all possible status codes
- Use ToParameters for query structs: Better documentation for complex queries
- Use
QueryParam instead of req.query(): QueryParam extractor automatically generates OpenAPI parameter documentation. req.query() manually parses query strings and will NOT appear in the generated OpenAPI spec. Always prefer QueryParam<T, REQUIRED> or ToParameters structs for query parameters.
Related Skills
- salvo-data-extraction: Extract request data with documentation
- salvo-error-handling: Document error responses
- salvo-auth: Document security requirements