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:
- Update the version in
Cargo.toml - Update CHANGELOG if you maintain one
- Commit and tag:
git tag v0.2.1 - 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 files —
cargo package --listshows what gets published. Exclude test fixtures, CI configs, and large assets withexcludeinCargo.toml. - Forgetting doc tests — the
# Examplessection in doc comments runs duringcargo test. If you skip examples, you lose Rust's strongest documentation guarantee. - No MSRV (minimum supported Rust version) — set
rust-versioninCargo.tomlso 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.