3 min read
On this page

Publishing Crates

The Rust ecosystem distributes libraries through crates.io. Publishing a crate makes it available to every Rust developer via cargo add. Doing it well means getting semver right, writing documentation that compiles, and providing feature flags so users only pay for what they need.

crates.io

crates.io is Rust's package registry. When you run cargo add serde, Cargo fetches the crate from crates.io. Anyone with a GitHub account can publish. Crate names are first-come, first-served and permanent — you cannot delete or rename a published crate.

Before publishing for the first time, log in:

cargo login <your-api-token>

Get your token from https://crates.io/settings/tokens.

Cargo.toml Metadata

A publishable crate needs metadata beyond the basics:

[package]
name = "my-parser"
version = "0.2.1"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "A fast, zero-copy parser for the XYZ format"
license = "MIT OR Apache-2.0"
repository = "https://github.com/you/my-parser"
homepage = "https://github.com/you/my-parser"
documentation = "https://docs.rs/my-parser"
readme = "README.md"
keywords = ["parser", "xyz", "zero-copy"]
categories = ["parsing", "encoding"]
rust-version = "1.70"

Required for publishing: name, version, description, license (or license-file). Everything else is strongly recommended — it helps users find and evaluate your crate.

Semantic Versioning

Rust follows semver strictly. Cargo uses it for dependency resolution:

0.1.0 -> 0.1.1    patch: bug fixes, no API changes
0.1.1 -> 0.2.0    minor: new features, backwards compatible
0.2.0 -> 1.0.0    major: breaking changes

Pre-1.0 special rules:
0.1.0 -> 0.2.0    treated as breaking (minor bump = major for 0.x)
0.1.0 -> 0.1.1    treated as compatible

Breaking changes include:

  • Removing a public function, struct, or method
  • Changing a function signature
  • Adding a required field to a public struct (unless it uses #[non_exhaustive])
  • Changing a trait's required methods

Use #[non_exhaustive] on public enums and structs to reserve the right to add variants or fields without a major version bump:

#[non_exhaustive]
pub enum ParseError {
    InvalidSyntax(String),
    UnexpectedEof,
}

Documentation with rustdoc

Rust's documentation system is built into the language. Doc comments compile as tests, which means your examples stay correct:

Doc comments with ///

/// Parses an XYZ-formatted string into a structured document.
///
/// # Arguments
///
/// * `input` - A string slice containing valid XYZ format
///
/// # Returns
///
/// A `Document` on success, or a `ParseError` if the input is malformed.
///
/// # Examples
///
/// ```
/// use my_parser::parse;
///
/// let doc = parse("key: value").unwrap();
/// assert_eq!(doc.get("key"), Some("value"));
/// ```
///
/// # Errors
///
/// Returns `ParseError::InvalidSyntax` if the input contains
/// malformed key-value pairs.
pub fn parse(input: &str) -> Result<Document, ParseError> {
    // ...
}

The # Examples block is compiled and run during cargo test. If the example does not compile or panics, your test suite fails. This is one of Rust's best features for documentation quality.

Module-level docs with //!

//! # my-parser
//!
//! A fast, zero-copy parser for the XYZ format.
//!
//! ## Quick start
//!
//! ```
//! use my_parser::parse;
//!
//! let doc = parse("key: value").unwrap();
//! assert_eq!(doc.get("key"), Some("value"));
//! ```

pub mod parser;
pub mod document;
pub mod error;

Building docs locally

cargo doc --open             # generate and open in browser
cargo doc --no-deps          # skip dependency docs for speed

docs.rs automatically builds documentation for every crate published to crates.io.

Feature Flags

Feature flags let users opt into functionality. They control conditional compilation and optional dependencies:

# Cargo.toml
[features]
default = ["json"]
json = ["dep:serde_json"]
yaml = ["dep:serde_yaml"]
async = ["dep:tokio"]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0.9", optional = true }
tokio = { version = "1", features = ["full"], optional = true }

Use features in code with cfg:

#[cfg(feature = "json")]
pub mod json {
    use serde_json;

    pub fn to_json<T: serde::Serialize>(value: &T) -> Result<String, serde_json::Error> {
        serde_json::to_string_pretty(value)
    }
}

#[cfg(feature = "yaml")]
pub mod yaml {
    pub fn to_yaml<T: serde::Serialize>(value: &T) -> Result<String, serde_yaml::Error> {
        serde_yaml::to_string(value)
    }
}

Users enable features in their Cargo.toml:

[dependencies]
my-parser = { version = "0.2", features = ["json", "yaml"] }

Feature design guidelines:

  • Default features should cover the common case
  • Features should be additive — enabling a feature should never remove functionality
  • Use dep: syntax to avoid implicit features from optional dependencies
  • Document which features exist and what they enable

Publishing Workflow

Step by step:

# 1. Make sure everything compiles and tests pass
cargo build
cargo test
cargo clippy
cargo doc --no-deps

# 2. Dry run to check for issues
cargo publish --dry-run

# 3. Verify the package contents
cargo package --list

# 4. Publish
cargo publish

Before publishing a new version:

  1. Update the version in Cargo.toml
  2. Update CHANGELOG if you maintain one
  3. Commit and tag: git tag v0.2.1
  4. Run cargo publish

Once published, a version is permanent. You can yank it (prevent new projects from depending on it) but not delete it:

cargo yank --version 0.2.0          # yank a broken release
cargo yank --version 0.2.0 --undo   # un-yank if you change your mind

Common Pitfalls

  • Publishing without cargo publish --dry-run — catches missing metadata, files excluded by .gitignore, and other issues before they are permanent.
  • Breaking semver accidentally — adding a public field to a struct is a breaking change. Use #[non_exhaustive] or builder patterns to maintain compatibility.
  • Undocumented features — if users cannot discover a feature flag, it does not exist. Document every feature in your crate-level docs.
  • Including unnecessary filescargo package --list shows what gets published. Exclude test fixtures, CI configs, and large assets with exclude in Cargo.toml.
  • Forgetting doc tests — the # Examples section in doc comments runs during cargo test. If you skip examples, you lose Rust's strongest documentation guarantee.
  • No MSRV (minimum supported Rust version) — set rust-version in Cargo.toml so users know which compiler they need.

Key Takeaways

  • Publishing to crates.io makes your library available to the entire Rust ecosystem. Get the metadata right.
  • Semver is not optional. Rust tooling depends on it for dependency resolution.
  • Doc comments with /// compile as tests. Write examples for every public function.
  • Feature flags let users opt into optional functionality. Keep them additive and documented.
  • Always dry-run before publishing. Versions are permanent.