Back to blog
|17 min read|Docsio

Rust Documentation: The Complete Guide for 2026

rust-documentationrustrustdocdeveloper-tools
Rust Documentation: The Complete Guide for 2026

Rust documentation is one of the things the Rust ecosystem genuinely got right. The language ships with rustdoc baked into the toolchain, cargo doc builds your docs without configuration, every public crate on crates.io gets auto-published to docs.rs, and your code examples run as tests on every push. Few other ecosystems can match that out of the box.

But "the tooling is good" doesn't mean writing the docs is automatic. A crate that compiles cleanly can still ship with empty /// summaries, missing # Errors sections, broken intra-doc links, and a lib.rs that tells nobody what the crate actually does. The gap between rustdoc-compliant docs and rustdoc docs people enjoy reading is where most Rust projects fall down, and it shows up in adoption.

This guide covers the tools the Rust ecosystem gives you, the conventions the standard library uses, when to reach for rustdoc vs mdBook, and how the user-facing layer (tutorials, integration guides, marketing-grade reference) sits on top of all that. If you also publish SDK documentation for a Rust binding, the same structure applies.

What rustdoc actually is (and what it isn't)

rustdoc is the official Rust documentation generator. It ships with every Rust toolchain installed through rustup, and you invoke it through Cargo: cargo doc --open builds HTML docs for your crate and every dependency, then opens them in your browser. There's no Doxyfile, no conf.py, no plugin ecosystem to configure. You write triple-slash comments, run one command, and get a documentation site.

What rustdoc is not: a general-purpose static site generator. It produces API reference. Tutorials, conceptual guides, architecture overviews, how-tos, marketing pages, integration walkthroughs: none of that belongs in /// comments on functions. For long-form prose, the Rust ecosystem reaches for mdBook, the same tool that builds The Rust Programming Language ("the Rust Book"). We'll get to when to use each.

The split matters. Mixing the two corrupts both. A lib.rs doc comment that tries to be a tutorial bloats the API reference; an mdBook that tries to be reference goes stale every time a function signature changes.

Documentation comments: /// vs //!

Rust has two flavors of documentation comments and they document different things.

Outer doc comments (///) attach to the item directly below them. Functions, structs, enums, traits, modules declared with mod foo {}. This is what you'll use most:

/// Returns the number of bytes in the buffer.
///
/// Equivalent to `self.buf.len()` but cheaper to call in hot paths
/// because it skips the validity check.
pub fn len(&self) -> usize {
    self.buf.len()
}

Inner doc comments (//!) document the item that contains them. They're how you write module-level and crate-level docs. Put them at the top of lib.rs to document the whole crate, or at the top of a foo.rs file to document the foo module:

//! # bytes_buffer
//!
//! A zero-copy byte buffer for binary protocols. Designed for hot
//! parsing loops where allocation is the bottleneck.
//!
//! See [`Buffer::new`] for the entry point.

pub struct Buffer { /* ... */ }

Both are written in Markdown. Rustdoc renders them to HTML. The crate-level //! block becomes the front page of your published docs, and it's the most important documentation you'll write because it's the first thing every reader sees on docs.rs. Don't waste it.

A third syntax exists, #[doc = "..."], which is the attribute form that /// and //! desugar to. You'll rarely write it by hand, but it shows up in macros that generate documented items.

What goes in a good doc comment

Rustdoc supports a small set of conventional sections, signaled by # Markdown headings. These aren't enforced by the compiler, but they're a convention the standard library follows and that every experienced Rust developer expects:

  • Summary line. First line, one sentence, ends with a period. Shows up in module-level listings and search results.
  • Body paragraphs. Optional. Explain what the item does, when to reach for it, anything counter-intuitive.
  • # Examples. Almost always include this. Examples compile and run as doc tests (more below).
  • # Errors. Required when a function returns Result<T, E>. Describe the conditions under which each Err variant occurs.
  • # Panics. Required when a function can panic. List the conditions.
  • # Safety. Required when a function is unsafe. Spell out the invariants the caller must uphold. Skipping this is a soundness bug.

The clippy::missing_errors_doc, clippy::missing_panics_doc, and clippy::missing_safety_doc lints will flag absent sections. Turn them on for library crates and the gaps surface during code review instead of after a user files a bug.

/// Parses a `Header` from the given bytes.
///
/// The header is expected to be 32 bytes, little-endian.
///
/// # Examples
///
/// ```
/// use my_crate::Header;
///
/// let bytes = [0u8; 32];
/// let header = Header::parse(&bytes).unwrap();
/// assert_eq!(header.version, 0);
/// ```
///
/// # Errors
///
/// Returns [`ParseError::TooShort`] if the slice is less than 32 bytes.
/// Returns [`ParseError::BadMagic`] if the leading 4 bytes are not `b"MYHD"`.
///
/// # Panics
///
/// Does not panic. All error cases return a `Result`.
pub fn parse(bytes: &[u8]) -> Result<Header, ParseError> { /* ... */ }

Skip # Arguments and # Returns. The argument names and the return type already convey that. The standard library doesn't use them, and they add noise without information.

Doc tests: code examples that compile

This is rustdoc's killer feature and the reason Rust documentation tends to stay accurate. Every fenced code block inside a doc comment is compiled and executed when you run cargo test:

/// Joins two strings with a separator.
///
/// ```
/// use my_crate::join;
/// assert_eq!(join("hello", "world", ", "), "hello, world");
/// ```
pub fn join(a: &str, b: &str, sep: &str) -> String { /* ... */ }

If join's signature changes and the example breaks, cargo test fails. The docs can't quietly drift out of sync with the code, which is one of the hardest problems in documentation maintenance on every other platform.

Doc test annotations:

AnnotationWhat it does
``` (no annotation)Compile and run as a test
```no_runCompile only, don't execute (use for networked or long-running code)
```compile_failCompile must fail (use to demonstrate type errors)
```should_panicCompile, run, expect a panic
```ignoreDon't compile or run (use sparingly, usually a code smell)
```textDon't treat as Rust at all (use for log output, file contents)

Lines starting with # inside a doc test are compiled but hidden from the rendered HTML. Use them to set up context without cluttering the example:

/// ```
/// # use my_crate::Buffer;
/// # let mut buf = Buffer::new();
/// buf.push(b"hello");
/// assert_eq!(buf.len(), 5);
/// ```

The reader sees three lines. The compiler sees five. The example still runs end-to-end.

For binary crates, cargo test does not run doc tests, because there's no library to link against. If you want doc tests in a binary, expose the testable logic in a library crate and have the binary depend on it. This is good architecture anyway.

Intra-doc links

Linking to other items in your crate (or in the standard library) used to require manual URLs. Since Rust 1.48, rustdoc resolves Rust paths inside Markdown links:

/// Returns a new [`Buffer`] backed by the given allocator.
///
/// See [`Buffer::push`] and [`AllocatorError`] for related types.
///
/// For the standard library equivalent, see [`std::vec::Vec`].
pub fn with_allocator<A: Allocator>(alloc: A) -> Buffer<A> { /* ... */ }

Rustdoc resolves Buffer, Buffer::push, AllocatorError, and std::vec::Vec to the right URLs at build time. If you rename Buffer to ByteBuffer, the links break loudly during cargo doc instead of silently rotting. Enable #![deny(rustdoc::broken_intra_doc_links)] at the crate root to make broken links a hard error.

The shorthand [Buffer] works for single items in scope. Use the disambiguating syntax for traits ([Buffer::push@trait]) or when you need a custom label ([the buffer](Buffer)).

Crate-level docs (lib.rs)

This is the most under-written part of most Rust crates. The //! block at the top of lib.rs is the front page of your docs.rs page. It's what every potential user reads first. Treat it as a landing page, not as a footnote.

A solid lib.rs header covers:

  1. One-line summary of what the crate is.
  2. A paragraph on what problem it solves and who it's for.
  3. A "getting started" example that compiles, with a real workflow, not let x = 1.
  4. Feature flag documentation if the crate has any (# Features).
  5. Links to the main types and modules a user will reach for first.
  6. Optional: MSRV (minimum supported Rust version), # Safety notes if the crate uses unsafe.

Look at how serde, tokio, reqwest, and clap write their lib.rs. The pattern is consistent. Open tokio's lib.rs and you'll see a few thousand lines of documentation comments before the first pub use. That's not overkill, that's the user manual.

//! # my_crate
//!
//! A zero-copy parser for the FrobnGuard binary protocol.
//!
//! ## Quick start
//!
//! ```
//! use my_crate::{Parser, Message};
//!
//! let bytes = std::fs::read("capture.bin").unwrap();
//! let mut parser = Parser::new(&bytes);
//! while let Some(msg) = parser.next_message()? {
//!     println!("{msg:?}");
//! }
//! # Ok::<(), my_crate::Error>(())
//! ```
//!
//! ## Features
//!
//! - `async`: enable async parsing via [`AsyncParser`]
//! - `serde`: implement `Serialize` / `Deserialize` on [`Message`]
//!
//! ## Modules
//!
//! - [`parser`]: the streaming parser
//! - [`message`]: message types and their variants
//! - [`error`]: error types

#![deny(missing_docs)]
#![deny(rustdoc::broken_intra_doc_links)]

Two lints at the bottom: missing_docs forces every public item to have a doc comment; broken_intra_doc_links catches stale links. Both belong in every library crate.

docs.rs: automatic publishing

docs.rs is the auto-publishing layer for crates.io. Every time you publish a release with cargo publish, docs.rs builds the documentation in a clean sandbox and hosts it at docs.rs/{crate}/{version}. You don't have to deploy anything. You don't host a site. You don't pay for it.

You can influence the build by adding a [package.metadata.docs.rs] table to Cargo.toml:

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"]

This is how you get the little badges that show feature-gated APIs ("available on crate feature async"). Combine it with #[cfg_attr(docsrs, doc(cfg(feature = "async")))] on your items and the docs visually signal which features unlock which APIs. It's a small touch and it dramatically improves discoverability.

Set package.documentation in Cargo.toml to point users to your docs.rs URL or to a custom doc site:

[package]
documentation = "https://docs.rs/my_crate"

mdBook: when rustdoc isn't enough

API reference is necessary, not sufficient. Users still need a tutorial that walks them through a real workflow, a conceptual overview that explains the mental model, and how-to guides for common tasks. None of that fits in /// comments.

mdBook is the Rust ecosystem's answer. It's the tool behind The Rust Programming Language, the Cargo Book, the Rustonomicon, the Embedded Rust Book, and most of the " Book" sites you've seen. It builds a multi-page docs site from Markdown files and a SUMMARY.md table of contents.

When to choose mdBook over rustdoc:

  • You need multi-page tutorials with their own URLs
  • You're documenting a domain, not just an API (a parser format, a protocol, a learning path)
  • You want a sidebar with custom navigation
  • You're shipping a book-shaped artifact

When to stick with rustdoc:

  • You're documenting a single library crate
  • The audience is developers reading from their IDE or from docs.rs
  • All your examples can live next to the code they describe

Most published Rust projects ship both: rustdoc for the API surface, an mdBook for the conceptual layer, and a landing page that points to each. Tokio is a good example to study. Its rustdoc lives on docs.rs and its mdBook ("Tokio Tutorial") lives at tokio.rs.

Setting up an mdBook

Install with Cargo: cargo install mdbook. Then in your project root:

mdbook init docs
cd docs
mdbook serve --open

mdbook serve runs a local dev server with live reload. The structure is opinionated and minimal:

docs/
├── book.toml          # config
└── src/
    ├── SUMMARY.md     # table of contents (required)
    ├── introduction.md
    ├── chapter_1.md
    └── chapter_2.md

SUMMARY.md is the load-bearing file. It defines navigation:

# Summary

- [Introduction](./introduction.md)
- [Getting Started](./getting-started.md)
  - [Installation](./getting-started/installation.md)
  - [First parse](./getting-started/first-parse.md)
- [Concepts](./concepts.md)
- [Reference](./reference.md)

Add mdbook-linkcheck to catch broken links in CI, and mdbook-mermaid if you want diagrams. The Rust Book and rustc-dev-guide both use mdbook-linkcheck in CI, and it catches dozens of rot bugs a year that would otherwise reach readers.

For deployment, GitHub Pages works out of the box: build with mdbook build, commit the book/ output (or use a CI action). Many Rust orgs publish at github.io/{repo} or at a custom subdomain.

Conventions the standard library follows

Reading the Rust standard library's source is the best way to internalize good documentation patterns. A few conventions worth copying:

Examples come early. Every pub fn in std::vec::Vec has an # Examples section, usually within the first few lines of the doc comment. Examples are the fastest way readers internalize a function.

Plural "Examples", even with one example. # Examples not # Example. Consistent grep targets, consistent rendering.

Errors documented in prose, not as a list. Look at std::fs::read. The # Errors section is a paragraph describing every error category, not a bulleted list. Bullets are tempting and almost always less informative.

Cross-link aggressively. Every type that references another type uses an intra-doc link. The standard library docs are a hypertext graph, not a linear document. That's why they're navigable.

Document modules. std's module-level //! blocks (look at std::collections or std::sync) explain what's in the module and when to reach for each type. New users land there from search and need the overview before they pick a type.

Hide implementation noise. Use #[doc(hidden)] on items that are public for technical reasons (macro internals, trait sealing) but shouldn't appear in user-facing docs.

Common patterns and gotchas

A few patterns that come up repeatedly in real Rust crates:

Feature-gated docs. Use #[cfg_attr(docsrs, doc(cfg(feature = "tokio")))] to make rustdoc show which feature unlocks an item. Pair with the [package.metadata.docs.rs] table above.

Re-exports break links. If you pub use foo::Bar from lib.rs, intra-doc links to Bar resolve to the re-export, not the original. Usually fine. Occasionally you want to link to the original location; use the full path [crate::foo::Bar].

Doc tests vs unit tests. Doc tests are slow to compile because each one is a separate compilation unit. Prefer unit tests for thorough coverage; use doc tests as readable usage examples. The standard library follows this split.

Hidden doctest setup. Use # lines to import types or set up state without polluting the rendered example. The reader sees the meaningful 3 lines, the compiler sees the full 10.

Don't document trait implementations. Document the trait itself. Users reading impl Display for MyType don't expect prose there; they expect a one-line summary of any custom behavior. This is a convention the Tangram Vision blog put well and it scales.

README and lib.rs duplication. Use #![doc = include_str!("../README.md")] to make lib.rs use the README as its crate-level docs. One source of truth, no drift. Most modern crates do this.

When rustdoc is the wrong tool

Rustdoc is the right tool for crate API reference. It's the wrong tool for everything around the API:

  • Product landing pages that introduce the project to non-Rust developers
  • Marketing-grade integration guides for SDK consumers
  • Quickstarts that compare against alternatives in the ecosystem
  • Hosted reference for HTTP APIs that happen to be built in Rust
  • Tutorials aimed at users who haven't installed Rust yet

For those, you need a real docs site. Most Rust projects that ship to non-Rust audiences (think: a CLI tool, a database, a hosted API) end up with three layers: rustdoc on docs.rs for the crate API, mdBook for in-depth Rust documentation, and a marketing-grade docs site for everything customer-facing. The third layer is where tools like Docsio fit. It generates the customer-facing layer from your existing site or marketing copy, leaving rustdoc and mdBook to do what they do well.

If you're building a developer tool in Rust, the customer-facing layer matters more than most teams admit. Stripe doesn't win on Rust's idiomatic documentation, it wins on the surrounding tutorials, the API explorer, and the integration walkthroughs. That stack is a different problem from rustdoc.

Best practices for Rust documentation

A short checklist that captures most of what experienced Rust crate authors do:

  1. Write the crate-level //! block first. It frames every other doc.
  2. Add #![deny(missing_docs)] to library crates. No public item ships undocumented.
  3. Add #![deny(rustdoc::broken_intra_doc_links)]. Links rot silently otherwise.
  4. Write doc tests, not snippets. If it can compile, make it compile.
  5. Use # Errors, # Panics, # Safety. Standard, expected, scannable.
  6. Skip # Arguments and # Returns. Names and types already cover them.
  7. Cross-link everything with intra-doc links. Build a graph, not a list.
  8. Set up [package.metadata.docs.rs] for feature-gated items. Free badges.
  9. For long-form docs, use mdBook. Don't stuff tutorials into lib.rs.
  10. Treat customer-facing docs as a separate problem. Different audience, different tool.

The Rust ecosystem rewards documentation discipline more than most. A well-documented crate gets more stars, more dependents, more issues filed instead of abandoned. That compounds.

Where to take it next

This guide covered the rustdoc + mdBook stack. The wider patterns that apply across any language (what to write, how to structure it, and where teams keep it) all reinforce the same point: ship the docs alongside the code, run them as tests, and treat the user-facing layer as its own product. If you're also building API documentation or SDK reference on top of a Rust binding, the docs-as-code workflow you build for rustdoc carries over.

Frequently asked questions

What's the difference between rustdoc and docs.rs? Rustdoc is the documentation generator that ships with the Rust toolchain. It produces HTML from your /// comments and Markdown. Docs.rs is a hosting service that runs rustdoc on every published crate from crates.io. You write docs with rustdoc; docs.rs publishes them for free.

Should I use rustdoc or mdBook? Both. Rustdoc owns the API reference because it lives next to the code and runs your examples as tests. mdBook owns long-form prose: tutorials, conceptual overviews, learning paths. Most established Rust projects ship rustdoc on docs.rs and an mdBook on a custom domain, with the landing page linking to each.

How do I write tested examples in Rust documentation? Put fenced code blocks inside /// comments. Every block compiles and runs when you call cargo test. Use # prefixes on setup lines you want hidden from the rendered output. Annotations like no_run, should_panic, and compile_fail cover edge cases.

Where should I host Rust documentation for my crate? If it's a library on crates.io, docs.rs hosts your API reference automatically. For an mdBook or marketing-grade docs site, GitHub Pages, Cloudflare Pages, or a hosted docs platform like Docsio all work. Use package.documentation in Cargo.toml to point users to wherever the canonical site lives.

Are doc tests run by cargo test? Yes, for library crates. Binary crates skip doc tests because rustdoc has no library to link against. If you need doc tests on a binary, extract the testable logic into a library crate and have the binary depend on it.

Ready to ship your docs?

Generate a complete documentation site from your URL in under 5 minutes.

Get Started Free