In the ecosystem of modern software development, code is read far more often than it is written. For Rust developers, this adage holds even more weight. The strictness of the borrow checker ensures memory safety, but it is documentation that ensures usability.
As we move through 2025, the standard for Rust crates has elevated significantly. It is no longer enough to simply publish code that compiles; if you want your library to be adopted by the community or easily maintained by your team, you need documentation that rivals the standard library.
Fortunately, Rust treats documentation as a first-class citizen. With rustdoc built directly into the toolchain, you don’t need external tools like Doxygen or Sphinx to get started.
In this guide, we will move beyond the basics. We will explore how to write documentation that is structurally sound, informative, and executable. You will learn the nuances of intra-doc links, the power of documentation tests, and how to configure your crate to enforce documentation standards.
Prerequisites #
To follow along with the code examples, ensure you have a standard Rust development environment set up:
- Rust Toolchain: Version 1.75 or newer (stable channel recommended).
- IDE: VS Code with
rust-analyzeror JetBrains RustRover. - Package Manager: Cargo (standard installation).
We will create a simple library crate to demonstrate these concepts. Open your terminal and run:
cargo new --lib doc_demo
cd doc_demo1. The Hierarchy of Comments #
In Rust, not all comments are created equal. Understanding the distinction between “code comments” and “documentation comments” is the first step.
The compiler treats comments starting with three slashes (///) or an exclamation mark (//!) as documentation. These are parsed and rendered into HTML.
Comparison of Comment Types #
Below is a breakdown of when to use which comment style:
| Syntax | Type | Target | Rendered in HTML? | Use Case |
|---|---|---|---|---|
// |
Standard | Implementation details | ❌ No | explaining how specific logic works inside a function body. |
/// |
Outer Doc | The item following it | ✅ Yes | Describing functions, structs, enums, and modules. |
//! |
Inner Doc | The item containing it | ✅ Yes | Top-level crate documentation or module-level summaries (usually at the top of a file). |
2. Anatomy of a Perfect Doc Comment #
A professional doc comment follows a specific structure: a short summary line, detailed description, and standard sections like Arguments, Returns, Panics, and Examples.
Let’s implement a robust function in our src/lib.rs and document it properly.
/// Calculates the specialized power of a base number.
///
/// This function is a wrapper around `pow` but adds checking for overflow
/// to ensure safety in financial calculations.
///
/// # Arguments
///
/// * `base` - The base integer.
/// * `exp` - The exponent to raise the base to.
///
/// # Returns
///
/// Returns `Some(u32)` if the calculation is successful, or `None` if an overflow occurs.
///
/// # Examples
///
/// ```
/// use doc_demo::safe_power;
///
/// let result = safe_power(2, 3);
/// assert_eq!(result, Some(8));
/// ```
pub fn safe_power(base: u32, exp: u32) -> Option<u32> {
base.checked_pow(exp)
}Key Components #
- Summary Line: The first paragraph should be a single line. This is what shows up in the list of functions on the main page of your documentation.
- Markdown Sections: Use H1 headers (
#) within the comment to create sections. The most common areArguments,Panics,Errors, andExamples. - Code Blocks: Indented or fenced code blocks are automatically highlighted as Rust code.
3. Doctests: The Killer Feature #
One of Rust’s greatest features is Doctests.
In the example above, the code inside the Examples section isn’t just text; it is a test case. When you run cargo test, Cargo extracts these code blocks, compiles them, and runs them.
This guarantees that your documentation never goes out of date. If you change your API but forget to update the docs, your tests will fail.
Handling Setup Code #
Sometimes you need setup code in your example that you don’t want to clutter the documentation with. You can hide lines using the # symbol at the start of the line.
/// Connects to a hypothetical database.
///
/// # Examples
///
/// ```
/// use doc_demo::Database;
/// # fn main() -> Result<(), String> {
/// // The line below is visible in docs:
/// let db = Database::connect("postgres://localhost")?;
/// # Ok(())
/// # }
/// ```
pub struct Database;
impl Database {
pub fn connect(_url: &str) -> Result<Self, String> {
Ok(Database)
}
}In the rendered HTML, the user only sees let db = Database::connect("postgres://localhost")?;, but the compiler sees the full main function wrapper needed to handle the Result.
4. The Documentation Flow #
Understanding how Rust processes your source code into a static HTML site helps in debugging issues.
5. Intra-doc Links #
In the past, linking to other parts of your crate involved guessing the HTML path (e.g., [MyStruct](../my_module/struct.MyStruct.html)). This was fragile.
Modern Rust uses Intra-doc links. You can use Rust paths directly in your markdown links.
/// Represents a user in the system.
pub struct User {
username: String,
}
/// A session associated with a [`User`].
///
/// When the session expires, the [`User`] is logged out.
/// You can also refer to the standard library, like [`Option`] or [`String`].
///
/// Use `mod@` to disambiguate if a module and item have the same name.
pub struct Session {
pub user: User,
}rustdoc will resolve [User] to the correct HTML page for the User struct. This works for items in your crate, dependencies, and the standard library.
6. Advanced Configuration and Lints #
To maintain a high bar for quality, you should configure your crate to reject undocumented code.
Enforcing Documentation #
Add the following attribute to the top of your lib.rs:
#
![warn(missing_docs)
]
// Or for the strictly masochistic:
// #
![deny(missing_docs)
]This triggers a compiler warning for any public item (function, struct, enum variant) that lacks a doc comment. It is an excellent way to ensure nothing slips through the cracks during code reviews.
Including External Markdown #
For your top-level crate documentation (the //! at the top of lib.rs), you often want to duplicate your README.md. Instead of copy-pasting, use the doc attribute:
#
![doc = include_str!("../README.md")
]This ensures your crates.io page and your GitHub repository README stay perfectly in sync.
7. Performance & Production Notes #
While documentation doesn’t affect runtime performance, it heavily impacts build time and developer velocity.
- Build Times: Generating docs for heavy dependencies can be slow. In CI/CD pipelines, consider if you need to run
cargo docon every commit or just on release branches. - Private Documentation: You can generate docs for private items (great for internal team onboarding) using:
cargo doc --document-private-items --open - Visuals:
rustdocsupports raw HTML. You can embed images or diagrams, but ensure they are hosted reliably. For 2025 standards, keep diagrams simple or use Mermaid.js integration via specific cargo extensions, though native support is still evolving.
Conclusion #
Great documentation is the difference between a library that is “usable” and one that is “lovable.” By leveraging Rust’s integrated rustdoc tool, specific markdown sections, and executable tests, you create a living contract with your users.
Actionable Advice:
- Turn on
# ![warn(missing_docs) ]in your current project today. - Review your public API and ensure every function has an
# Examplessection. - Use intra-doc links to make navigation seamless.
Writing docs is writing code. Treat it with the same respect, and your crate’s quality will soar.
Further Reading: