How to Document Public APIs in Rust

Best Practices

Document public Rust APIs by adding `///` doc comments with examples and running `cargo doc` to generate the reference.

You just finished a crate

You just finished a crate that handles image resizing. You publish it to crates.io. A week later, an issue pops up: "How do I pass the output format?" You stare at your code. The function signature says process(path: &str, fmt: u8). You know fmt is 0 for PNG and 1 for JPEG because you wrote it. The user doesn't. They're guessing. They're frustrated. You're frustrated. The gap between what's in your head and what's in the code is where bugs live. Rust has a built-in way to close that gap. It turns your comments into a searchable website and runs your examples as tests.

Doc comments are the user manual

Doc comments are the user manual that lives inside the code. Think of a vending machine. The buttons have labels: "A1: Chips", "B2: Soda". If the labels fall off, you're guessing. You might press a button and get nothing. Or worse, you get a broken snack. Doc comments are those labels. They tell the user what a function does, what arguments it expects, and what it returns.

In Rust, they do something extra. They double as tests. If the example in the comment doesn't compile or run, your documentation is broken. The compiler forces you to keep the manual up to date. You can't ship a crate with a manual that lies.

Minimal example

Use /// to attach documentation to an item. The comment must sit directly above the item. Rust extracts the text and the code blocks.

/// Calculates the sum of two integers.
///
/// # Examples
///
/// ```
/// // The # prefix hides this line in the rendered docs.
/// // It keeps the example clean without cluttering the output.
/// # use my_crate::add;
///
/// let result = add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    // Simple addition. The doc comment above explains the purpose.
    a + b
}

Run cargo doc --open to generate the HTML and open it in your browser. You'll see the text rendered with formatting. The code block appears as a runnable example. The # line is invisible in the HTML but present during compilation. This keeps examples concise. Users see the logic, not the boilerplate.

Run cargo doc --open to see the result. The HTML is your first draft of the user experience.

How cargo doc works

When you run cargo doc, the tool scans every /// comment attached to a public item. It strips the slashes, preserves the formatting, and extracts code blocks. It treats those code blocks as real Rust code. It compiles them. It runs them. If an example fails, cargo doc fails. You can't publish broken docs.

The output is a set of HTML files. You get search, cross-references, and a clean layout. The convention is to run cargo doc --open during development to check how things look. Many developers keep the docs open in a tab while coding. It catches missing docs and broken examples instantly.

The compiler enforces truth. If the example breaks, the build breaks.

Realistic API structure

Real APIs have structure. You document structs, impls, and modules. Use /// for items. Use //! for module-level docs. The //! comment applies to the parent module. Place it at the top of a file or inside a module block.

//! A library for database connections.
//!
//! This crate provides a safe wrapper around raw TCP sockets.
//! It handles connection pooling and automatic retries.
//!
//! # Quick start
//!
//! ```
//! // Import the main struct.
//! # use my_db::Connection;
//!
//! let conn = Connection::new("localhost:5432");
//! ```

/// A handle to a database connection.
///
/// This struct manages the lifecycle of a TCP connection.
/// It drops the connection when the handle goes out of scope.
///
/// # Examples
///
/// ```
/// // Import the struct. The # hides this from the rendered output.
/// # use my_db::Connection;
///
/// let conn = Connection::new("localhost:5432");
/// // The connection closes automatically here.
/// ```
pub struct Connection {
    // Internal fields are private. Users interact via methods.
    host: String,
}

impl Connection {
    /// Opens a connection to the specified host.
    ///
    /// The host string must include the port number.
    ///
    /// # Panics
    ///
    /// Panics if the host string is empty.
    ///
    /// # Examples
    ///
    /// ```
    /// # use my_db::Connection;
    /// let conn = Connection::new("db.example.com:5432");
    /// ```
    pub fn new(host: &str) -> Self {
        if host.is_empty() {
            // Documented panic. Users can search for "Panics" to find this.
            panic!("host cannot be empty");
        }
        Self {
            host: host.to_string(),
        }
    }
}

The # Panics section is a community standard. Users expect to find panic conditions there. If a function can panic, document it. If you don't, users assume it won't. The //! comment at the top describes the crate's purpose. It appears on the root page of the docs.

Treat the # Panics section as a promise. If you panic, document it. If you don't document it, users assume you won't.

Smart links and attributes

Rust docs support smart links. You can link to types and functions using bracket syntax. The compiler resolves these links. If the target doesn't exist, you get a warning.

/// Returns a [String] containing the result.
///
/// See [std::string::String] for details.
pub fn get_result() -> String {
    String::from("ok")
}

Use #[doc(alias = "old_name")] to help users find renamed items. This adds search aliases. Users searching for the old name will find the new one.

/// Calculates the hash.
///
/// Previously named `compute_hash`.
#[doc(alias = "compute_hash")]
pub fn calculate_hash(data: &[u8]) -> u64 {
    // Implementation details.
    0
}

For crates, add #![doc(html_root_url = "https://docs.rs/my-crate/1.0.0")] to lib.rs. This fixes links to other crates in the generated HTML. Without it, links to external types might break when users view the docs offline.

Convention aside: The community expects # Examples headers in doc comments. It's not required by the compiler, but it's the standard format used by std and most popular crates. Follow it. It makes your docs feel familiar.

Keep links alive. If a link breaks, the docs feel abandoned.

Pitfalls and broken examples

Common mistakes trip up new developers. Forgetting to hide imports makes docs ugly. Examples that don't compile break the build. Using no_run when you should run wastes the test harness.

If you forget the # on an import, the rendered docs show boilerplate. Users see use my_crate::add; instead of just the logic. It clutters the example. If the example has a bug, cargo doc stops with a compilation error. You'll see something like E0432 (use of undeclared crate or module) if the import path is wrong. Or E0601 (expected specifier, found ) if the syntax is malformed.

Use no_run in code blocks when the code requires external setup. This tells the compiler to compile but not execute the example.

/// Connects to the production database.
///
/// # Examples
///
/// ```no_run
/// // This code requires a running database.
/// // The no_run attribute skips execution but checks compilation.
/// # use my_db::Connection;
/// let conn = Connection::new("prod-db.example.com:5432");
/// ```

Use ignore only when the example contains pseudocode or syntax that won't compile. This is rare in Rust. Prefer fixing the example or using no_run instead. ignore disables all checking. It hides bugs.

Convention aside: Run cargo test --doc in CI. This runs all doc tests. It ensures examples stay valid as the API evolves. Broken examples are just as bad as broken code. Users copy examples. If they fail, they lose trust.

Run cargo test --doc in CI. Broken examples are just as bad as broken code.

Decision matrix

Use /// for documenting public items like functions, structs, enums, and traits. This attaches the comment to the item and includes it in the generated HTML.

Use //! for module-level documentation. Place this at the top of a file or inside a module block to describe the module's role.

Use #[doc(hidden)] for items that must be public for compilation but aren't part of the user-facing API. This hides them from the generated docs while keeping them accessible to code.

Use no_run in code examples when the snippet requires external resources. This applies when you need a running database, a specific file on disk, or a network connection that the test environment can't provide.

Use ignore when the example contains pseudocode or syntax that won't compile. This is rare in Rust; prefer fixing the example or using no_run instead.

Pick the tool that matches the intent. Hidden items stay hidden. Module docs describe scope. Item docs describe behavior.

Where to go next