| Crates.io | rovo |
| lib.rs | rovo |
| version | 0.3.1 |
| created_at | 2025-11-18 12:58:24.066939+00 |
| updated_at | 2026-01-14 09:22:25.737411+00 |
| description | A drop-in replacement for axum::Router with effortless OpenAPI documentation |
| homepage | |
| repository | https://github.com/Arthurdw/rovo |
| max_upload_size | |
| id | 1938417 |
| size | 624,456 |
OpenAPI documentation for Axum using doc comments and macros.
Built on aide, Rovo provides a declarative approach to API documentation through special annotations in doc comments.
axum::Router.get(), .post(), .patch(), .delete())use rovo::{rovo, Router, routing::get};
use rovo::{schemars, schemars::JsonSchema};
use rovo::{aide, aide::{axum::IntoApiResponse, openapi::OpenApi}};
use rovo::{extract::State, response::Json};
use serde::Serialize;
#[derive(Clone)]
struct AppState {}
#[derive(Serialize, JsonSchema)]
struct User {
id: u64,
name: String,
}
/// Get user information.
///
/// Returns the current user's profile information.
///
/// # Responses
///
/// 200: Json<User> - User profile retrieved successfully
///
/// # Metadata
///
/// @tag users
#[rovo]
async fn get_user(State(_state): State<AppState>) -> impl IntoApiResponse {
Json(User {
id: 1,
name: "Alice".to_string(),
})
}
#[tokio::main]
async fn main() {
let state = AppState {};
let mut api = OpenApi::default();
api.info.title = "My API".to_string();
let app = Router::new()
.route("/user", get(get_user))
.with_oas(api)
.with_swagger("/")
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
[dependencies]
rovo = { version = "0.3.1", features = ["swagger"] }
axum = "0.8"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
Note: Rovo re-exports
aide,schemars, and common axum types (extract,response,http), so you can import them directly from rovo. Theaxumdependency is still needed foraxum::serve().
For detailed API documentation, see docs.rs/rovo.
Choose one or more documentation UIs (none enabled by default):
swagger - Swagger UIredoc - Redoc UIscalar - Scalar UIRovo uses Rust-style documentation with markdown sections and metadata annotations.
/// Get a todo item by ID.
///
/// Retrieves a single todo item from the database. Returns 404
/// if the item doesn't exist.
///
/// # Path Parameters
///
/// id: The todo item's unique identifier
///
/// # Responses
///
/// 200: Json<TodoItem> - Successfully retrieved the todo item
/// 404: () - Todo item was not found
/// 500: Json<ErrorResponse> - Internal server error
///
/// # Examples
///
/// 200: TodoItem {
/// title: "Buy milk".into(),
/// ..Default::default()
/// }
/// 404: ()
///
/// # Metadata
///
/// @tag todos
/// @security bearer_auth
#[rovo]
async fn get_todo(Path(id): Path<i32>) -> impl IntoApiResponse {
// ...
}
Document HTTP responses with status codes, types, and descriptions:
/// # Responses
///
/// 200: Json<User> - User found successfully
/// 404: () - User not found
/// 500: Json<ErrorResponse> - Internal server error
Format: <status_code>: <type> - <description>
Document path parameters for primitive types:
/// # Path Parameters
///
/// id: The user's unique identifier
/// index: Zero-based item index
Format: <name>: <description>
String, u64, u32, i64, i32, bool, Uuid, etc.Path((a, b)): Path<(Uuid, u32)>, document each parameterProvide concrete response examples:
/// # Examples
///
/// 200: User { id: 1, name: "Alice".into(), email: "alice@example.com".into() }
/// 404: ()
Format: <status_code>: <rust_expression>
Examples should match the types defined in the Responses section.
Contains API metadata using @ annotations:
@tagGroup operations by tags (can be used multiple times):
/// # Metadata
///
/// @tag users
/// @tag authentication
@securitySpecify security requirements (can be used multiple times):
/// # Metadata
///
/// @security bearer_auth
Security schemes must be defined in your OpenAPI spec. See Tips for details.
@idSet custom operation ID (defaults to function name):
/// # Metadata
///
/// @id getUserById
Must contain only alphanumeric characters and underscores.
@hiddenHide an operation from documentation:
/// # Metadata
///
/// @hidden
#[deprecated]Mark endpoints as deprecated using Rust's built-in attribute:
#[deprecated]
#[rovo]
async fn old_handler() -> impl IntoApiResponse {
// ...
}
@rovo-ignoreStop processing annotations after this point (location-independent):
/// Get user information.
///
/// # Responses
///
/// 200: Json<User> - User found successfully
///
/// # Metadata
///
/// @tag users
///
/// @rovo-ignore
///
/// Additional documentation here won't be processed.
/// You can write @anything without causing errors.
#[rovo]
async fn handler() -> impl IntoApiResponse {
// ...
}
use rovo::Router;
let app = Router::new()
.route("/path", get(handler))
.with_state(state);
use rovo::routing::{get, post, patch, delete};
Router::new()
.route("/items", get(list_items).post(create_item))
.route("/items/{id}", get(get_item).patch(update_item).delete(delete_item))
Router::new()
.nest(
"/api",
Router::new()
.route("/users", get(list_users))
.route("/posts", get(list_posts))
)
Router::new()
.route("/users", get(list_users))
.with_oas(api)
.with_swagger("/swagger")
.with_redoc("/redoc")
.with_scalar("/scalar")
.with_state(state)
Use custom OAS route:
Router::new()
.route("/users", get(list_users))
.with_oas_route(api, "/openapi")
.with_swagger("/")
.with_state(state)
Rovo automatically serves your OpenAPI specification in multiple formats:
/api.json (default)/api.yaml or /api.ymlAll formats are automatically available when you use .with_oas() or .with_oas_route().
See examples/todo_api.rs for a complete CRUD API.
Run with:
cargo run -F swagger --example todo_api
Replace imports and add documentation:
// Before
use axum::{Router, response::IntoResponse, routing::get};
async fn handler() -> impl IntoResponse {
Json(data)
}
// After
use rovo::{Router, routing::get, schemars::JsonSchema};
use rovo::{aide::axum::IntoApiResponse, response::Json};
/// Handler description
///
/// # Responses
///
/// 200: Json<Data> - Success
///
/// # Metadata
///
/// @tag category
#[rovo]
async fn handler() -> impl IntoApiResponse {
Json(data)
}
Add OpenAPI setup in main():
use rovo::aide::openapi::OpenApi;
let mut api = OpenApi::default();
api.info.title = "My API".to_string();
let app = Router::new()
.route("/path", get(handler))
.with_oas(api)
.with_swagger("/")
.with_state(state);
For primitive types (String, u64, Uuid, bool, etc.), document parameters directly in doc comments:
/// Get user by ID.
///
/// # Path Parameters
///
/// id: The user's unique identifier
///
/// # Responses
///
/// 200: Json<User> - User found
#[rovo]
async fn get_user(Path(id): Path<u64>) -> impl IntoApiResponse {
// ...
}
For tuple parameters:
/// Get item in collection.
///
/// # Path Parameters
///
/// collection_id: The collection UUID
/// index: Item index
///
/// # Responses
///
/// 200: Json<Item> - Item found
#[rovo]
async fn get_item(Path((collection_id, index)): Path<(Uuid, u32)>) -> impl IntoApiResponse {
// ...
}
For complex types, use structs with JsonSchema:
use rovo::schemars::JsonSchema;
use serde::Deserialize;
use uuid::Uuid;
#[derive(Deserialize, JsonSchema)]
struct UserId {
/// The user's unique identifier
id: Uuid,
}
#[rovo]
async fn get_user(Path(UserId { id }): Path<UserId>) -> impl IntoApiResponse {
// ...
}
Define in OpenAPI object:
use rovo::aide::openapi::{SecurityScheme, SecuritySchemeData};
api.components.get_or_insert_default()
.security_schemes
.insert(
"bearer_auth".to_string(),
SecurityScheme {
data: SecuritySchemeData::Http {
scheme: "bearer".to_string(),
bearer_format: Some("JWT".to_string()),
},
..Default::default()
},
);
Reference in handlers:
/// Protected endpoint requiring authentication.
///
/// # Responses
///
/// 200: Json<Data> - Success
/// 401: () - Unauthorized
///
/// # Metadata
///
/// @security bearer_auth
#[rovo]
async fn protected_handler() -> impl IntoApiResponse {
// ...
}
Add the #[rovo] macro:
#[rovo]
async fn handler() -> impl IntoApiResponse {
// ...
}
.with_state()Add explicit type annotation:
let router: Router<()> = Router::<AppState>::new()
.route("/path", get(handler))
.with_state(state);
| Feature | aide | rovo |
|---|---|---|
| Documentation | Separate _docs function |
Doc comments |
| Routing | api_route() |
Native axum syntax |
| Method chaining | Custom | Standard axum |
| Lines per endpoint | ~15-20 | ~5-10 |
This project uses just for common development tasks.
# List all available commands
just
# Run all checks (format, clippy, tests)
just check
# Fix formatting and clippy issues
just fix
# Run tests
just test
# Check for outdated dependencies
just outdated
# Check for unused dependencies
just unused-deps
# Check for security vulnerabilities
just audit
Uses prek for git hooks:
prek install
prek run # Run manually
just test - Run all testsjust lint - Run clippy lintsjust fmt - Format codejust check - Run all checks (fmt, clippy, test)just fix - Run all checks and fixesjust build - Build the projectjust example - Run the todo_api examplejust outdated - Check for outdated dependenciesjust unused-deps - Check for unused dependenciesjust audit - Check for security vulnerabilitiesjust docs - Build and open documentationjust pre-release - Run all pre-release checksSee just --list for all available commands.
Contributions are welcome. Please submit a Pull Request.
MIT