How to Write Good Documentation for Rust Crates

Write Rust crate docs using rustdoc comments for APIs and mdbook for guides, then build with cargo doc and mdbook build.

The blank README problem

You spend three weeks building a parsing library. It handles edge cases, runs fast, and passes every test. You publish it to crates.io. Two days later, a user opens an issue asking how to actually use it. The repository has a blank README. The source code has no comments. The user closes the issue and finds a different crate. Good code stays hidden without good documentation.

Rust splits documentation into two distinct tools. rustdoc generates API reference pages directly from your source code. mdbook builds standalone user guides from markdown files. Think of rustdoc as a dictionary. It tells you exactly what each function does, what it accepts, and what it returns. Think of mdbook as a cookbook. It shows you how to combine ingredients to solve a real problem. You need both. One explains the pieces. The other explains the recipe.

Writing API docs that actually help

Start with rustdoc. Place three slashes above any public item. The compiler treats those lines as documentation, not code. The first line acts as a summary. Keep it under 80 characters. The following lines explain edge cases, invariants, or return values.

/// Calculates the average of a list of numbers.
/// 
/// Returns `None` if the slice is empty to avoid division by zero.
/// This function allocates no memory and runs in linear time.
pub fn average(numbers: &[f64]) -> Option<f64> {
    // Return early to prevent a panic on empty input
    if numbers.is_empty() {
        return None;
    }
    // Sum the slice using an iterator to avoid manual indexing
    let sum: f64 = numbers.iter().sum();
    // Cast length to f64 for floating point division
    Some(sum / numbers.len() as f64)
}

Run cargo doc --open. Rust compiles the crate, extracts every /// block, and renders an HTML site. Click the function name. You see the description, the signature, and generated links to standard library types. The convention in the Rust community is to write the first line as a standalone summary. The following paragraphs explain behavior. Do not repeat the function signature. The compiler already prints it. Focus on intent and constraints.

Module-level documentation uses a different syntax. Place //! at the top of a file to document the entire module. This appears on the module's landing page. It should explain the module's purpose, list public types, and point to examples. Keep it brief. Users scan module pages to find the right tool. They do not read novels.

Doctests: documentation that runs

rustdoc does more than copy text. It parses markdown, highlights Rust code blocks, and runs them as tests. When you add a code example inside the comment, Rust treats it as a doctest.

/// Calculates the average of a list of numbers.
/// 
/// # Examples
/// 
/// ```
/// // Import the function from the current crate
/// use my_crate::average;
/// 
/// let scores = [85.0, 92.0, 78.0];
/// let avg = average(&scores);
/// // Verify the calculation matches expected output
/// assert_eq!(avg, Some(85.0));
/// ```
/// 
/// Empty slices return `None` to prevent division by zero.
/// 
/// ```
/// use my_crate::average;
/// let empty: &[f64] = &[];
/// assert_eq!(average(empty), None);
/// ```
pub fn average(numbers: &[f64]) -> Option<f64> {
    if numbers.is_empty() {
        return None;
    }
    let sum: f64 = numbers.iter().sum();
    Some(sum / numbers.len() as f64)
}

Run cargo test --doc. Rust extracts every fenced code block, wraps it in a fn main(), and compiles it. If the example fails to compile or panics, your build breaks. This keeps documentation honest. Outdated examples become impossible to ship. The community treats doctests as first-class tests. They verify that the public API actually works the way you claim it does.

You can hide setup code from the rendered documentation while keeping it in the test. Add a # at the start of the line. rustdoc strips it from the HTML but leaves it in the compiled test. This keeps examples clean without sacrificing correctness.

/// ```
/// # use my_crate::average;
/// let scores = [85.0, 92.0, 78.0];
/// let avg = average(&scores);
/// assert_eq!(avg, Some(85.0));
/// ```

Structure your doctests with standard headings. # Examples shows basic usage. # Panics documents conditions that trigger a panic. # Safety explains why an unsafe block is sound. These headings are not arbitrary. rustdoc recognizes them and renders them with distinct styling. Users scan for them. Give them what they look for.

Building a user guide with mdbook

A library usually needs more than function signatures. Users want to know how to configure the library, handle errors, and integrate it into a larger project. That is where mdbook takes over. Create a docs directory. Add a book.toml configuration file.

[book]
# Set the author and language for metadata
authors = ["Your Name"]
language = "en"
multilingual = false
# Point to the directory containing markdown chapters
src = "src"
title = "My Crate Guide"

[output.html]
# Link back to the source repository for issue tracking
git-repository-url = "https://github.com/you/my-crate"
# Enable search indexing for the generated site
search = true

The src directory holds a SUMMARY.md file that defines the navigation tree.

# Summary

- [Introduction](intro.md)
- [Getting Started](getting-started.md)
- [Configuration](configuration.md)
- [Advanced Usage](advanced.md)

Write each chapter as a standard markdown file. Run mdbook build. The tool compiles the markdown into a static site with search, syntax highlighting, and a responsive sidebar. Deploy the output to GitHub Pages, Netlify, or any static host. The convention is to keep the guide focused on workflows. Do not repeat the API reference. Link to rustdoc output when you need to show a specific method signature. Use relative links to jump between chapters. Absolute links break when you move the repository.

Integrate mdbook into your CI pipeline. Run mdbook test to check for broken links and missing images. Run mdbook build before deployment. Treat the guide as a deliverable. It ships alongside your crate. Users judge your project by the quality of its instructions.

Where documentation breaks

Documentation drifts faster than code. You update a function signature but forget to update the guide. The guide shows a deprecated flag. The README promises features that landed in a different branch. Prevent drift by running cargo test --doc in your CI pipeline. Failing doctests block merges. Another trap is over-documenting internals. Private functions do not appear in rustdoc output. Writing /// comments on fn helper() wastes time and clutters the source. Reserve documentation for public APIs and complex algorithms. If a function is three lines long and named clearly, skip the comment. The code is the documentation.

Compiler errors in doctests look identical to regular test failures. Rust reports them as doc-test my_crate::average. The stack trace points to the generated test file, not your source. Read the line numbers carefully. They map back to the comment block. If you see a "cannot find value" error, you likely forgot to import a type inside the example block. Doctest blocks are isolated. They do not inherit use statements from the module. Write self-contained examples.

Another common failure is ignoring the #![doc] attributes. You can control how rustdoc renders your crate. Add #![doc(html_root_url = "...")] to your crate root to fix broken links when users view your docs on docs.rs. Add #![no_std] if your crate targets embedded environments. These attributes change the generated output. Test them locally before publishing.

Choosing your documentation strategy

Use rustdoc for API references when you need to document public functions, structs, traits, and modules. Use rustdoc when you want examples to run as automated tests. Use mdbook for user guides when you need to explain workflows, architecture, or setup steps. Use mdbook when your audience includes beginners who need narrative context. Use a README.md for quick installation instructions and a high-level overview. Use inline // comments for implementation details that only maintainers need to understand.

The Rust community follows a few unwritten rules. Always write doctest examples that compile without warnings. Add # at the start of lines you want to hide from the rendered documentation but keep in the test. This keeps examples clean while preserving necessary setup code. Run cargo doc --no-deps to skip documenting standard library and dependency crates. It speeds up local builds and keeps the output focused on your code. Treat documentation as part of the API contract. Breaking a documented behavior is a semver violation, even if the compiler does not catch it.

Write documentation for the person who will use your crate at 2 AM. They will not read your commit history. They will not guess your intent. They will read your comments. Make them count.

Where to go next