Skip to main content
โšก Calmops

Documentation and API Documentation in Rust: rustdoc, OpenAPI, and Best Practices

Documentation and API Documentation in Rust: rustdoc, OpenAPI, and Best Practices

TL;DR: This guide covers creating comprehensive documentation for Rust projects. You’ll learn rustdoc syntax, doc tests, OpenAPI/Swagger generation, and documentation workflows that improve developer experience.


Introduction

Good documentation is crucial for any library or API. Rust provides excellent tooling for documentation:

  • rustdoc - Built-in documentation generator
  • Doc tests - Documentation that runs as tests
  • OpenAPI/Swagger - REST API documentation
  • MDBook - For writing books and guides

rustdoc Basics

Documenting Functions

/// Performs a fuzzy search on the given query.
///
/// # Arguments
///
/// * `query` - The search query string
/// * `options` - Optional search configuration
///
/// # Returns
///
/// Returns a `Vec<SearchResult>` containing matching items,
/// sorted by relevance score.
///
/// # Examples
///
/// ```
/// use my_search_lib::{fuzzy_search, SearchOptions};
///
/// let results = fuzzy_search("rust", SearchOptions::default());
/// assert!(!results.is_empty());
/// ```
pub fn fuzzy_search(query: &str, options: SearchOptions) -> Vec<SearchResult> {
    // Implementation
}

Documenting Structs and Enums

/// Represents a user in the system.
///
/// # Fields
///
/// * `id` - Unique identifier
/// * `username` - User's login name  
/// * `email` - User's email address
/// * `role` - User's role in the system
///
/// # Example
///
/// ```
/// use my_app::User;
///
/// let user = User::new(
///     "john_doe".to_string(),
///     "[email protected]".to_string(),
/// );
/// ```
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    /// Unique identifier for the user
    pub id: Uuid,
    
    /// User's login name (unique)
    pub username: String,
    
    /// User's email address
    pub email: Email,
    
    /// User's role in the system
    pub role: UserRole,
}

Documenting Enums

/// Represents the status of an order in the system.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderStatus {
    /// Order has been placed but not yet confirmed
    Pending,
    
    /// Order has been confirmed and is being processed
    Confirmed,
    
    /// Order is currently being prepared
    Processing,
    
    /// Order is ready for pickup/shipping
    Ready,
    
    /// Order has been shipped
    Shipped,
    
    /// Order has been delivered
    Delivered,
    
    /// Order has been cancelled
    Cancelled,
}

Documentation Sections

Using Sections

/// Calculate the fibonacci number at position n.
///
/// # Panics
///
/// Panics if n is greater than 93 (due to overflow).
///
/// # Safety
///
/// This function is safe for all valid inputs within
/// the u64 range, but will overflow for n > 93.
///
/// # See Also
///
/// * [`fibonacci_iterative`] - Iterative version
/// * [`fibonacci_matrix`] - Matrix exponentiation version
pub fn fibonacci_recursive(n: u64) -> u64 {
    if n <= 1 {
        n
    } else {
        fibonacci_recursive(n - 1) + fibonacci_recursive(n - 2)
    }
}

Common Section Headers

Header Purpose
# or # Arguments Describe function parameters
# Returns Describe return value
# Errors Describe error conditions
# Panics Describe panic conditions
# Safety Safety invariants for unsafe functions
# Examples Runnable code examples
# See Also Related functions/types

Doc Tests

Writing Doc Tests

/// Adds two numbers together.
///
/// # Examples
///
/// ```
/// use my_math::add;
///
/// assert_eq!(add(2, 3), 5);
/// assert_eq!(add(10, -5), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Ignoring Tests

/// Complex example that's slow to run.
///
/// ```
/// # // The # prefix hides this from the rendered docs
/// use my_lib::heavy_computation;
///
/// let result = heavy_computation();
/// // ... assertions
/// ```
pub fn heavy_computation() -> Result<HeavyResult, Error> {
    // Implementation
}

Using compile_fail

/// This function expects a non-empty string.
///
/// ```compile_fail
/// use my_lib::process_name;
///
/// let result = process_name(""); // This won't compile
/// ```
pub fn process_name(name: &str) -> Result<Name, Error> {
    if name.is_empty() {
        Err(Error::EmptyName)
    } else {
        Ok(Name(name.to_string()))
    }
}

OpenAPI/Swagger Generation

Using utoipa

[dependencies]
utoipa = { version = "4.0", features = ["actix_extras", "yaml"] }
utoipa-swagger-ui = "6.0"

Basic OpenAPI Schema

use utoipa::{OpenApi, ToSchema};
use serde::{Deserialize, Serialize};

#[derive(OpenApi)]
#[openapi(
    info(
        title = "My API",
        version = "1.0.0",
        description = "A sample API built with Rust",
        contact(
            name = "API Support",
            email = "[email protected]"
        ),
        license(
            name = "MIT",
            url = "https://opensource.org/licenses/MIT"
        )
    ),
    servers(
        server("http://localhost:8080")
            .description("Development server"),
        server("https://api.example.com")
            .description("Production server")
    ),
    components(
        schemas(
            User, 
            CreateUserRequest,
            UpdateUserRequest,
            ErrorResponse
        )
    ),
    paths(
        user::list_users,
        user::create_user,
        user::get_user,
        user::update_user,
        user::delete_user
    )
)]
pub struct ApiDoc;

#[derive(ToSchema, Serialize, Deserialize)]
#[schema(example = json!({
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "username": "john_doe",
    "email": "[email protected]",
    "role": "user"
}))]
pub struct User {
    #[schema(example = "550e8400-e29b-41d4-a716-446655440000")]
    pub id: String,
    
    #[schema(example = "john_doe")]
    pub username: String,
    
    #[schema(example = "[email protected]")]
    pub email: String,
    
    #[schema(example = "user")]
    pub role: UserRole,
}

Path Definitions

use utoipa::ToSchema;
use utoipa::openapi::path::{Operation, Response, Get, Post, Delete, PathItem};

/// List all users
///
/// Returns a paginated list of users.
#[utoipa::path(
    get,
    path = "/users",
    tag = "Users",
    responses(
        (status = 200, description = "List of users", body = [User]),
        (status = 401, description = "Unauthorized", body = ErrorResponse),
        (status = 500, description = "Internal server error", body = ErrorResponse)
    ),
    params(
        ("page" = i32, Query, description = "Page number"),
        ("per_page" = i32, Query, description = "Items per page (max 100)")
    ),
    security(
        ("bearer_auth" = [])
    )
)]
pub async fn list_users(
    State(state): State<AppState>,
    Query(params): Query<PaginationParams>,
) -> Result<Json<Vec<User>>, AppError> {
    let users = state.db.users()
        .paginate(params.page, params.per_page)
        .await?;
    
    Ok(Json(users))
}

/// Create a new user
#[utoipa::path(
    post,
    path = "/users",
    tag = "Users",
    request_body = CreateUserRequest,
    responses(
        (status = 201, description = "User created", body = User),
        (status = 400, description = "Invalid request", body = ErrorResponse),
        (status = 409, description = "User already exists", body = ErrorResponse)
    )
)]
pub async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUserRequest>,
) -> Result<Json<User>, AppError> {
    let user = state.db.create_user(payload).await?;
    Ok(Json(user))
}

Adding Swagger UI

use utoipa_swagger_ui::SwaggerUi;

fn configure_routes() -> impl Filter<Extract = (impl Reply,), Error = Infallible> + Clone {
    let api = ApiDoc::openapi();
    
    Router::new()
        .route("/api/users", get(list_users))
        .route("/api/users", post(create_user))
        .merge(SwaggerUi::new("/swagger-ui/**").url("/api-docs/openapi.json", api))
}

MDBook for Guides

Setting Up MDBook

cargo install mdbook
mdbook init my-book

Configuration

# book.toml
[book]
title = "My Library Guide"
description = "Documentation for my Rust library"
authors = ["Your Name"]

[build]
src = "src"
create-missing = false

[preprocessor.theme]
renderer = "markdown"

[output.html]
git-repository-url = "https://github.com/username/repo"
edit-url-template = "https://github.com/username/repo/edit/main/{path}"

Writing Content

# Getting Started

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
my_lib = "1.0"

Basic Usage

use my_lib::MyStruct;

fn main() {
    let instance = MyStruct::new();
    instance.do_something();
}

API Reference

For detailed API documentation, see the Rust API docs.


---

## Documentation Best Practices

### 1. Write Examples First

```rust
/// Creates a new client with the specified configuration.
///
/// # Examples
///
/// ```rust
/// use my_client::Client;
///
/// let client = Client::builder()
///     .timeout(Duration::from_secs(30))
///     .retries(3)
///     .build();
/// ```
pub fn builder() -> ClientBuilder {
    ClientBuilder::new()
}

2. Use Consistent Style

/// Gets the user's profile.
///
/// Use this to retrieve profile information for display purposes.
/// For updating profiles, see [`update_profile`].
///
/// Returns `None` if the user doesn't exist.
pub async fn get_profile(id: &str) -> Option<Profile>;

3. Document Error Cases

/// Fetches a resource by ID.
///
/// # Errors
///
/// Returns [`Error::NotFound`] if the resource doesn't exist.
/// Returns [`Error::Unauthorized`] if the user lacks permissions.
/// Returns [`Error::RateLimited`] if too many requests were made.
pub async fn fetch_resource(id: &str) -> Result<Resource, Error>;

4. Keep Documentation Close to Code

// lib.rs
//! My Library
//!
//! This library provides functionality for...
//!
//! # Usage
//!
//! ```rust
//! use my_lib::...
//! ```

mod client;
//! Client for connecting to the API

Automation

GitHub Actions for Docs

# .github/workflows/docs.yml
name: Documentation

on:
  push:
    branches: [main]

jobs:
  doc:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install Rust
        uses: dtolnay/rust-action@stable
      
      - name: Build docs
        run: cargo doc --no-deps
      
      - name: Deploy docs
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./target/doc

Cargo.toml Documentation

[package]
name = "my-lib"
version = "1.0.0"
edition = "2021"
description = "A short description of your library"
documentation = "https://docs.rs/my-lib/"
repository = "https://github.com/username/my-lib"
license = "MIT"
keywords = ["rust", "library", "tags"]
categories = ["api-bindings", "asynchronous"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--markdown-markdown-header-style=atom"]

Generating Reference Documentation

Documenting Macros

/// A macro to create a new instance with default values.
///
/// # Example
///
/// ```
/// use my_lib::new_instance;
///
/// let instance = new_instance! {
///     field1: "value1",
///     field2: 42,
/// };
/// ```
#[macro_export]
macro_rules! new_instance {
    ($($field:ident: $value:expr),* $(,)?) => {
        MyStruct {
            $($field: $value),*
        }
    };
}

Documenting Traits

/// A trait for types that can be serialized.
///
/// Implementors of this trait must ensure that
/// the serialization is reversible.
pub trait Serializable {
    /// Serialize the type to a byte vector.
    fn serialize(&self) -> Result<Vec<u8>, SerializationError>;
    
    /// Deserialize from a byte vector.
    fn deserialize(data: &[u8]) -> Result<Self, SerializationError>
    where
        Self: Sized;
}

Conclusion

Effective documentation combines:

  1. rustdoc - Code-level documentation with examples
  2. Doc tests - Examples that double as tests
  3. OpenAPI - REST API specification
  4. MDBook - Guides and tutorials
  5. Automation - CI/CD for documentation

Invest in documentation earlyโ€”it compounds over time.


External Resources


Comments