Arthurdw
rovo
Rust

Axum open API docs made simple

Last updated Jul 2, 2026
91
Stars
7
Forks
1
Issues
0
Stars/day
Attention Score
35
Language breakdown
Rust 92.4%
Lua 2.5%
TypeScript 2.2%
Kotlin 1.7%
Just 1.0%
JavaScript 0.1%
โ–ธ Files click to expand
README

Rovo - OpenAPI for Axum, the easy way

Crates.io Documentation CI codecov License: MIT


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:
- VSCode/VSCodium - See VSCODE.md for extension with auto-installation, completions, and syntax highlighting - Neovim LSP - See NEOVIM.md for editor support with completions, hover docs, and more - JetBrains IDEs - See JETBRAINS.md for RustRover, IntelliJ IDEA, and CLion support - Zed - See ZED.md for full Zed 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-exports aide, schemars, and common axum types (extract, response, http), so you can import them directly from rovo. The axum dependency is still needed for axum::serve().

For detailed API documentation, see docs.rs/rovo.

Feature Flags

Choose one or more documentation UIs (none enabled by default):

  • swagger - Swagger UI
  • redoc - Redoc UI
  • scalar - 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.yaml or /api.yml
All formats are automatically available when you use .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 tests
  • just lint - Run clippy lints
  • just fmt - Format code
  • just check - Run all checks (fmt, clippy, test)
  • just fix - Run all checks and fixes
  • just build - Build the project
  • just example - Run the todo_api example
  • just outdated - Check for outdated dependencies
  • just unused-deps - Check for unused dependencies
  • just audit - Check for security vulnerabilities
  • just docs - Build and open documentation
  • just pre-release - Run all pre-release checks
See just --list for all available commands.

Contributing

Contributions are welcome. Please submit a Pull Request.

License

MIT

๐Ÿ”— More in this category

ยฉ 2026 GitRepoTrend ยท Arthurdw/rovo ยท Updated daily from GitHub