Crates & Packages in Rust
Before you can understand Cargo, modules, or how Rust projects are structured, you need to know what a crate and a package are. These two terms are precise and distinct in Rust — and they are the foundation of everything else.
What is a Crate?
A crate is the smallest unit of compilation in Rust. Every time you run
cargo build, the compiler processes one crate at a time. A crate is either:
- A binary crate — has a
mainfunction, produces an executable. - A library crate — has no
main, produces a.rlibfile that other crates can link against.
The file where the compiler starts reading is called the crate root.
Crate type | Crate root | Output |
|---|---|---|
Binary crate | src/main.rs | Executable binary |
Library crate | src/lib.rs | .rlib (linkable library) |
What is a Package?
A package is one or more crates managed together by Cargo. A package:
- Contains a
Cargo.tomlfile that describes the package's metadata and dependencies. - Can contain at most one library crate (
src/lib.rs). - Can contain multiple binary crates (
src/main.rsplus anything undersrc/bin/).
When you run cargo new my_project, Cargo creates a package with a single binary
crate rooted at src/main.rs.
$ cargo new my_project
Created binary (application) `my_project` package
$ cargo new my_lib --lib
Created library `my_lib` packageBinary Crates
A binary crate compiles to an executable that can be run directly. It must contain
a main function as its entry point. The default binary crate root is
src/main.rs:
// src/main.rs — the crate root for the default binary
fn main() {
println!("Hello from a binary crate!");
}$ cargo run
Compiling my_project v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/my_project`
Hello from a binary crate!Library Crates
A library crate exposes functions, types, and traits for other crates to use.
It has no main function. The crate root is src/lib.rs:
// src/lib.rs — the crate root for a library crate
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn new(x: f64, y: f64) -> Self {
Point { x, y }
}
pub fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}src/lib.rs and src/main.rs. In that case, the binary crate can use the library crate internally by name. This is a common pattern for CLI tools that also expose a library API.Multiple Binary Crates with src/bin/
A package can contain multiple binary crates by placing source files in the
src/bin/ directory. Each file becomes a separate binary:
my_project/
├── Cargo.toml
└── src/
├── lib.rs ← library crate
├── main.rs ← default binary (my_project)
└── bin/
├── server.rs ← extra binary (my_project-server)
└── client.rs ← extra binary (my_project-client)# Run the default binary cargo run # Run a specific binary from src/bin/ cargo run --bin server cargo run --bin client
The [[bin]] Section in Cargo.toml
For more control over binary names and paths, declare them explicitly in
Cargo.toml using the [[bin]] array of tables (double brackets mean an array):
[package] name = "my_project" version = "0.1.0" edition = "2021" [[bin]] name = "server" path = "src/server_main.rs" [[bin]] name = "client" path = "src/client_main.rs" [lib] name = "my_project_lib" path = "src/lib.rs"
The Crate Root
The crate root is the file the Rust compiler starts from when building a crate.
It is the top of the module tree for that crate. Everything the crate contains
must be reachable from this file through a chain of mod declarations or inline
code.
- Binary crate root:
src/main.rs(or the path in[[bin]]) - Library crate root:
src/lib.rs(or the path in[lib])
External Crates as Dependencies
Rust's package registry is crates.io. To use an external crate,
add it to the [dependencies] section of your Cargo.toml, then run
cargo build to download and compile it:
[dependencies]
rand = "0.8"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }// src/main.rs
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
let n: u32 = rng.gen_range(1..=100);
println!("Random number: {}", n);
}extern crate rand;. Adding the dependency in Cargo.toml is enough — the crate is available automatically via use.The extern crate Syntax (Pre-2018)
In Rust editions before 2018, you had to explicitly declare external crates at the
crate root with extern crate. You may still see this in older code:
// Old style (edition 2015) — rarely needed today extern crate serde; extern crate rand; use rand::Rng;
edition = "2021"in your Cargo.toml. The extern crate declaration is only required in 2021 for the special macro_rules! crate import pattern, which is also increasingly rare.Crate Naming Conventions
snake_case for crate names:
my_library,json_parser,web_serverCargo converts hyphens in package names to underscores for use in Rust code:
my-libin Cargo.toml becomesmy_libinusestatementsKeep names short and descriptive — they appear in every
usepathAvoid generic names like
utilsorhelpers— be specific about what the crate does
Private vs Public APIs in a Library Crate
A library crate's public API is everything marked pub in src/lib.rs and its
submodules. Items without pub are private to the crate and cannot be accessed
by downstream users.
Designing a good public API means:
Expose types and functions that users genuinely need
Hide implementation details — they can change without breaking callers
Use
pub useto re-export deeply nested types at a convenient pathDocument everything public with
///doc commentsConsider stability: once published, removing public items is a breaking change
// src/lib.rs
// Public — part of the library's API
pub struct Config {
pub host: String,
pub port: u16,
}
// Public constructor — hides the struct's internal complexity
impl Config {
pub fn new(host: &str, port: u16) -> Self {
Config {
host: host.to_string(),
port,
}
}
}
// Private — implementation detail, invisible to callers
fn validate_port(port: u16) -> bool {
port > 1024
}
// pub(crate) — shared across this crate's own modules, not exported
pub(crate) fn internal_setup() {
println!("internal setup");
}Crate vs Module — Key Differences
Concept | Crate | Module |
|---|---|---|
Definition | A compilation unit; produces a binary or library | A namespace within a crate |
Boundary | Defined by the crate root file | Defined by mod blocks or separate files |
Privacy boundary | pub(crate) stops here | pub(super) refers to parent module |
Cargo involvement | Listed in Cargo.toml | No Cargo involvement |
Can be published | Yes — to crates.io | No — modules live inside a crate |
Publishing Your Own Crate
Publishing a library crate to crates.io makes it available for anyone to use as a dependency. The basic flow is:
- Write and test your library.
- Add required metadata to
Cargo.toml:description,license,repository. - Log in:
cargo login(requires a crates.io account and API token). - Publish:
cargo publish.
See the dedicated crates-io page for the full publishing workflow, semantic versioning, and how to yank a bad release.
[package] name = "my_awesome_lib" version = "0.1.0" edition = "2021" description = "A short description of what the library does" license = "MIT OR Apache-2.0" repository = "https://github.com/you/my_awesome_lib" readme = "README.md" keywords = ["parsing", "json"] categories = ["parser-implementations"]
Cargo.toml. External crates are added as dependencies and accessed via use. A library crate's public API is everything explicitly marked pub — everything else is an implementation detail hidden from the outside world.