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:
- rustdoc - Code-level documentation with examples
- Doc tests - Examples that double as tests
- OpenAPI - REST API specification
- MDBook - Guides and tutorials
- Automation - CI/CD for documentation
Invest in documentation earlyโit compounds over time.
Comments