Skip to content
ExponaBLOG
RUST PROGRAMMING · DAY 066 / 100

Publishing crates: what every Rust developer should know

Share libraries on crates.io.

Expona Admin9 min read

Introduction

You've built a solid Rust library, tested it thoroughly, and now you want the world to use it. Publishing crates is what every Rust developer should know—it's the bridge between a working project and a community resource. Getting your library onto crates.io requires more than just running a command; it demands attention to versioning, documentation, and user experience.

In this guide, we'll walk through the complete publishing workflow, from preparing your crate for release to ensuring developers can find, understand, and trust your code. Whether you're publishing your first library or your tenth, these practices will help you avoid pitfalls and establish a solid foundation for maintenance.

Understanding cargo publish

The cargo publish command is your primary tool for uploading a crate to crates.io. Before you can publish, you need a crates.io account and an API token to authenticate your uploads.

Start by creating an account at crates.io and generating an API token from your account settings. Then configure your local environment:

rust
cargo login

This command prompts you to paste your API token. Cargo stores it securely and uses it for all future publish operations.

Before publishing, ensure your Cargo.toml is complete. Here's what a production-ready manifest looks like:

rust
[package]
name = "request-cache"
version = "1.0.0"
edition = "2021"
authors = ["Your Team"]
description = "A high-performance HTTP request caching layer"
license = "MIT"
repository = "https://github.com/yourorg/request-cache"
documentation = "https://docs.rs/request-cache"
readme = "README.md"
keywords = ["http", "cache", "performance"]
categories = ["web-programming", "caching"]

[dependencies]
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }

[dev-dependencies]
tokio-test = "0.4"

The license, description, and repository fields are required for publication. The keywords and categories help users discover your crate through search.

When you're ready, run:

rust
cargo publish --dry-run

This simulates the publish process without actually uploading. It catches missing metadata, documentation, and other issues early. Once the dry run succeeds, publish for real:

rust
cargo publish

Crates.io immediately makes your version available. This action is permanent—you cannot delete a published version, only yank it (mark it as unsuitable for new projects). Always verify your crate works before publishing.

Semantic versioning: getting it right

Semantic versioning (semver) is the contract between you and your users. It signals what kind of changes you've made and whether they need to update their code. The format is MAJOR.MINOR.PATCH.

MAJOR increments when you make breaking changes—removing a public function, changing a public struct, or altering API signatures. MINOR increments when you add new functionality in a backward-compatible way. PATCH increments for bug fixes that don't change the API.

Consider a crate that provides a data validation library. Version 1.0.0 launches with basic field validators:

rust
pub struct Validator {
    pub max_length: Option,
}

impl Validator {
    pub fn validate(&self, input: &str) -> Result<(), String> {
        if let Some(max) = self.max_length {
            if input.len() > max {
                return Err("too long".to_string());
            }
        }
        Ok(())
    }
}

You add email validation in version 1.1.0—a new public function that doesn't affect existing code:

rust
impl Validator {
    pub fn validate_email(&self, email: &str) -> Result<(), String> {
        // implementation
        Ok(())
    }
}

Later, you discover a bug where validate doesn't handle UTF-8 properly. You fix it in version 1.1.1:

rust
impl Validator {
    pub fn validate(&self, input: &str) -> Result<(), String> {
        // fixed UTF-8 handling
        Ok(())
    }
}

Then you redesign the API to be more ergonomic, removing the Validator struct and replacing it with a builder pattern. This is version 2.0.0—a breaking change:

rust
pub struct ValidatorBuilder { /* ... */ }

impl ValidatorBuilder {
    pub fn max_length(mut self, len: usize) -> Self { /* ... */ self }
    pub fn build(self) -> Validator { /* ... */ }
}

Users depending on request-cache = "1" automatically get 1.1.1 but not 2.0.0, protecting their builds from breaking changes. This is why semver matters—it enables safe dependency management across ecosystems.

Publishing documentation on docs.rs

Documentation builds automatically on docs.rs when you publish to crates.io. However, documentation quality depends on your code. Write clear doc comments for all public items, and docs.rs renders them beautifully.

Here's an example of well-documented code:

rust
/// A high-performance cache for HTTP responses.
///
/// This cache respects cache-control headers and evicts stale entries
/// automatically. It's thread-safe and suitable for use in async contexts.
///
/// # Examples
///
/// ```
/// use request_cache::ResponseCache;
///
/// #[tokio::main]
/// async fn main() {
///     let cache = ResponseCache::new(1000);
///     // cache operations here
/// }
/// ```
pub struct ResponseCache {
    entries: Arc>>,
    max_entries: usize,
}

impl ResponseCache {
    /// Creates a new cache with the specified maximum number of entries.
    ///
    /// # Arguments
    ///
    /// * `max_entries` - Maximum number of responses to cache
    ///
    /// # Panics
    ///
    /// Panics if `max_entries` is 0.
    pub fn new(max_entries: usize) -> Self {
        assert!(max_entries > 0, "max_entries must be positive");
        Self {
            entries: Arc::new(Mutex::new(HashMap::new())),
            max_entries,
        }
    }

    /// Retrieves a cached response by URL key.
    ///
    /// Returns `None` if the key doesn't exist or the cached entry has expired.
    pub async fn get(&self, key: &str) -> Option {
        // implementation
        None
    }
}

Include examples in your doc comments. The Rust documentation renderer extracts these as testable code—they serve as both documentation and regression tests. If your example breaks in a future release, your docs tests fail, alerting you to incompatibilities.

Docs.rs also respects a docs.rs configuration in your Cargo.toml. Use it to customize feature flags or metadata for documentation builds:

rust
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

This ensures docs.rs builds with all optional features enabled, giving users a complete API reference. Conditional compilation attributes like #[cfg(doc)] can highlight unstable features or experimental APIs.

Crafting an excellent README and examples

Your README is the first impression. It appears on crates.io, GitHub, and in documentation aggregators. A strong README answers: What problem does this crate solve? How do I use it? What are the key features?

Here's a README template that works across projects:

rust
# request-cache

A high-performance HTTP response cache for Rust applications.

## Features

- Thread-safe, async-friendly caching
- Automatic expiration based on cache-control headers
- Configurable eviction policies
- Zero-copy response streaming

## Installation

Add this to your `Cargo.toml`:

```toml
[dependencies]
request-cache = "1.0"
```

## Quick Start

```rust
use request_cache::ResponseCache;

#[tokio::main]
async fn main() -> Result<()> {
    let cache = ResponseCache::new(1000);
    
    let response = cache.get_or_fetch(
        "https://api.example.com/data",
        || async { /* fetch logic */ }
    ).await?;
    
    println!("{:?}", response);
    Ok(())
}
```

## Performance

Benchmarks on a 4-core system show 100k operations/sec with our LRU eviction strategy.

## License

Licensed under MIT.

Keep your README concise. Link to full documentation on docs.rs for detailed information. Provide working code examples that users can copy and adapt immediately.

Beyond the README, create an examples/ directory with standalone programs demonstrating key workflows:

rust
// examples/basic_cache.rs

use request_cache::ResponseCache;

#[tokio::main]
async fn main() {
    let cache = ResponseCache::new(100);
    
    // First call fetches from the source
    let data = cache.get_or_fetch(
        "https://httpbin.org/json",
        || async {
            // Simulated fetch
            Ok("response".to_string())
        }
    ).await.unwrap();
    
    println!("Cached: {}", data);
}

Users run examples with cargo run --example basic_cache. This validates your API and demonstrates best practices in action.

Common mistakes

Publishing without testing: Always run cargo test and cargo publish --dry-run before the real publish. A broken release damages trust instantly.

Incomplete Cargo.toml metadata: Missing descriptions, licenses, or repositories make your crate harder to discover and evaluate. Fill every field; it takes minutes and significantly improves perception.

Yanking versions too liberally: Yanking signals that a version is broken, but it doesn't retroactively update existing lockfiles. Users with pinned versions won't know about the yank. Reserve yanking for genuinely unsafe releases; consider patching with a new minor or patch version instead.

Breaking changes in minor versions: Violating semver erodes community trust. If you accidentally publish a breaking change as a minor bump, yank it immediately and republish as a major version.

Neglecting examples and documentation: A published crate with poor docs is a published problem. Invest time in clear examples and comprehensive doc comments. They pay dividends in adoption and support burden reduction.

Ignoring docs.rs build failures: Check docs.rs after publishing. If documentation fails to build, your crate appears broken to potential users. Missing dependencies or feature flag issues often cause this.

Key takeaways

  • cargo publish uploads your crate to crates.io; always use --dry-run first to validate metadata and dependencies.
  • Semantic versioning (MAJOR.MINOR.PATCH) signals breaking changes, new features, and bug fixes to users; respect it religiously to maintain ecosystem trust.
  • Docs.rs automatically builds and hosts documentation from your doc comments and examples; write clear, tested examples to guide users.
  • A strong README and examples directory lower the barrier to adoption and reduce support questions; invest in both.
  • Incomplete Cargo.toml metadata, missing documentation, and casual semver violations are the most common pitfalls; address all three before publishing.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles