Skip to content

Arthurdw/rovo

Repository files navigation

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 - See VSCODE.md for Visual Studio Code 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

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 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(); }

Installation

[dependencies] rovo = { version = "0.2.5", 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. /// /// # 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

Examples Section

Provide 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.

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);

Method Chaining

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))

Nesting Routes

Router::new() .nest( "/api", Router::new() .route("/users", get(list_users)) .route("/posts", get(list_posts)) )

Documentation UIs

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)

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 .with_oas() or .with_oas_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);

Tips

Path Parameters

Use structs with JsonSchema:

use rovo::schemars::JsonSchema; use serde::Deserialize; use uuid::Uuid; #[derive(Deserialize, JsonSchema)] struct UserId { 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.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 { // ... }

Troubleshooting

Handler doesn't implement required traits

Add the #[rovo] macro:

#[rovo] async fn handler() -> impl IntoApiResponse { // ... }

Type mismatch with .with_state()

Add explicit type annotation:

let router: Router<()> = Router::<AppState>::new() .route("/path", get(handler)) .with_state(state);

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

About

Axum open API docs made simple

Topics

Resources

License

Stars

Watchers

Forks

Contributors 4

  •  
  •  
  •  
  •