Axum open API docs made simple
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.
Features
- Drop-in replacement for
axum::Router - Doc-comment driven documentation
- Compile-time validation of annotations
- Method chaining support (
.get(),.post(),.patch(),.delete()) - Built-in Swagger/Redoc/Scalar UI integration
- Type-safe response definitions
- Minimal runtime overhead
- Editor Integration:
Quick Start
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 getuser(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) .finish();
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap();
axum::serve(listener, app).await.unwrap(); }
Installation
[dependencies]
rovo = { version = "0.4.8", features = ["swagger"] }
axum = "0.8"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
Note: Rovo re-exportsaide,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.
Feature Flags
Choose one or more documentation UIs (none enabled by default):
swagger- Swagger UIredoc- Redoc UIscalar- Scalar UI
Documentation Format
Rovo uses Rust-style documentation with markdown sections and metadata annotations.
Complete Example
/// 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 {
// ...
}
Responses Section
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>
- Status codes must be valid HTTP codes (100-599)
- Type must be valid Rust syntax
- Description explains when this response occurs
Path Parameters Section
Document path parameters for primitive types:
/// # Path Parameters
///
/// id: The user's unique identifier
/// index: Zero-based item index
Format: <name>: <description>
- Parameter names must match the variable bindings in your function signature
- Works with primitives:
String,u64,u32,i64,i32,bool,Uuid, etc. - For tuple paths like
Path((a, b)): Path<(Uuid, u32)>, document each parameter
Examples Section
Provide concrete response examples:
/// # Examples
///
/// 200: User { id: 1, name: "Alice".into(), email: "alice@example.com".into() }
/// 404: ()
Format: <statuscode>: <rustexpression>
Examples should match the types defined in the Responses section.
Metadata Section
Contains API metadata using @ annotations:
@tag
Group operations by tags (can be used multiple times):
/// # Metadata
///
/// @tag users
/// @tag authentication
@security
Specify security requirements (can be used multiple times):
/// # Metadata
///
/// @security bearer_auth
Security schemes must be defined in your OpenAPI spec. See Tips for details.
@id
Set custom operation ID (defaults to function name):
/// # Metadata
///
/// @id getUserById
Must contain only alphanumeric characters and underscores.
@hidden
Hide an operation from documentation:
/// # Metadata
///
/// @hidden
Special Directives
#[deprecated]
Mark endpoints as deprecated using Rust's built-in attribute:
#[deprecated]
#[rovo]
async fn old_handler() -> impl IntoApiResponse {
// ...
}
@rovo-ignore
Stop 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 {
// ...
}
Router API
Basic Usage
use rovo::Router;
let app = Router::new() .route("/path", get(handler)) .with_state(state) .finish();
Method Chaining
use rovo::routing::{get, post, patch, delete};
Router::new() .route("/items", get(listitems).post(createitem)) .route("/items/{id}", get(getitem).patch(updateitem).delete(delete_item))
Nesting Routes
Router::new()
.nest(
"/api",
Router::new()
.route("/users", get(list_users))
.route("/posts", get(list_posts))
)
Nest routers with different state types by calling .with_state() on each inner router:
let app = Router::new()
.nest(
"/api",
Router::new()
.route("/todos", get(list_todos))
.withstate(todostate),
)
.nest(
"/meta",
Router::new()
.route("/health", get(health_check))
.withstate(metastate),
)
.with_oas(api)
.finish();
Documentation UIs
Router::new()
.route("/users", get(list_users))
.with_oas(api)
.with_swagger("/swagger")
.with_redoc("/redoc")
.with_scalar("/scalar")
.with_state(state)
.finish()
Use custom OAS route:
Router::new()
.route("/users", get(list_users))
.withoasroute(api, "/openapi")
.with_swagger("/")
.with_state(state)
.finish()
OpenAPI Formats
Rovo automatically serves your OpenAPI specification in multiple formats:
- JSON -
/api.json(default) - YAML -
/api.yamlor/api.yml
.withoas() or .withoas_route().
Examples
See examples/todo_api.rs for a complete CRUD API.
Run with:
cargo run -F swagger --example todo_api
Migration from Axum
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) .finish();
Tips
Path Parameters
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 getitem(Path((collectionid, 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 { // ... }
Security Schemes
Define in OpenAPI object:
use rovo::aide::openapi::{SecurityScheme, SecuritySchemeData};
api.components.getorinsert_default() .security_schemes .insert( "bearerauth".tostring(), SecurityScheme { data: SecuritySchemeData::Http { scheme: "bearer".to_string(), bearerformat: Some("JWT".tostring()), }, ..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 {
// ...
}
Troubleshooting
Handler doesn't implement required traits
Add the #[rovo] macro:
#[rovo]
async fn handler() -> impl IntoApiResponse {
// ...
}
Using different state types
Call .with_state() on each nested router individually, then .finish() at the top level (see Nesting Routes).
Comparison with aide
| 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 |
Development
This project uses just for common development tasks.
Quick Start
# 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
Pre-commit Hooks
Uses prek for git hooks:
prek install
prek run # Run manually
Available Commands
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 checks
just --list for all available commands.
Contributing
Contributions are welcome. Please submit a Pull Request.
License
MIT