Crates Packages and Modules in Rust Told Apart Explained
A package has a Cargo.toml, a crate is one compilation unit, and a module is a namespace inside it. Learn how the three nest and what each one changes.

Four words get used loosely in Rust conversations: crate, package, module and workspace. They are four different things at four different levels, and mixing them up is why error messages like "unresolved import" or "could not determine which binary to run" feel opaque. The cleanest way to keep them apart is to build one small project that has all four, look at what Cargo says about it, and break it on purpose. All outputs are from Rust 1.99, edition 2024.
The four words
A crate is what the compiler (rustc) compiles in one run. It is either a library, which produces an .rlib, or a binary, which produces an executable. Every crate has a root source file, and the compiler discovers the rest by following mod declarations from that file.
A module is a namespace inside a crate. It exists to group items and control visibility, and it costs nothing at run time.
A package is what Cargo manages. It is described by one Cargo.toml, and it can contain a library crate (at most one) and any number of binary crates, plus tests, examples and benchmarks.
A workspace is a set of packages developed together. They share one Cargo.lock and one target directory.
So the hierarchy, from largest to smallest, is workspace, package, crate, module. The rest of this post is one project with all four.
A project with two packages
The layout on disk:
ws/
Cargo.toml <- workspace root
crates/
geometry/ <- package "geometry" (a library)
Cargo.toml
src/lib.rs <- crate root of the library
app/ <- package "app"
Cargo.toml
src/main.rs <- crate root of binary crate "app"
src/bin/extra.rs <- crate root of binary crate "extra"The workspace root has a Cargo.toml that lists its members and holds settings they share:
[workspace]
resolver = "3"
members = ["crates/geometry", "crates/app"]
[workspace.package]
edition = "2024"
version = "0.1.0"resolver = "3" selects the dependency resolver that matches edition 2024. [workspace.package] holds values that members can inherit, so every member says edition.workspace = true and cannot drift to a different edition. The first package, geometry, is a library:
[package]
name = "geometry"
version.workspace = true
edition.workspace = true
[dependencies]//! The library crate: its root file is `src/lib.rs`, and everything else is a module in it.
pub mod shapes {
pub fn square_area(side: f64) -> f64 {
side * side
}
pub fn circle_area(r: f64) -> f64 {
super::PI_ISH * r * r
}
}
const PI_ISH: f64 = std::f64::consts::PI;
pub use shapes::square_area; // re-export: callers can write geometry::square_areaThe file src/lib.rs is the crate root of the library crate. pub mod shapes { ... } declares a module inside it, with two public functions. PI_ISH is a private constant in the crate root, and the child module reaches it as super::PI_ISH, because a child module may use its parent's private items. The last line, pub use shapes::square_area;, is a re-export: it makes square_area available at geometry::square_area as well as at geometry::shapes::square_area, which is how libraries present a tidy top level while organising code in deeper modules.
The second package, app, depends on the first by path:
[package]
name = "app"
version.workspace = true
edition.workspace = true
[dependencies]
geometry = { path = "../geometry" }// A binary crate: this file is the crate root. `geometry` is a *different crate*.
use geometry::shapes::circle_area;
mod report {
// A module inside the app crate, in the same file.
pub fn line(label: &str, value: f64) -> String {
format!("{label}: {value:.2}")
}
}
fn main() {
println!("{}", report::line("square", geometry::square_area(3.0)));
println!("{}", report::line("circle", circle_area(1.0)));
}use geometry::shapes::circle_area; reaches into another crate, and it works only because circle_area is pub and so is the module containing it. Inside app, mod report { ... } is a module in the same file, used as report::line(...). Those are the two kinds of path in one file: a path across a crate boundary, and a path inside one crate. Running the app:
square: 9.00
circle: 3.14What Cargo thinks the project is
Cargo can describe its model of the project. Asking for package metadata and printing the targets gives the cleanest picture of the vocabulary:
package geometry 0.1.0
crate geometry kind=lib root=crates/geometry/src/lib.rs
package app 0.1.0
crate app kind=bin root=crates/app/src/main.rs
crate extra kind=bin root=crates/app/src/bin/extra.rsThere are two packages, geometry and app. geometry has one crate, of kind lib, whose root is src/lib.rs. app has two crates of kind bin: app from src/main.rs and extra from src/bin/extra.rs. A file under src/bin is automatically a separate binary crate with the file's name; no entry in Cargo.toml is needed. Both binaries are in the same package, share its dependencies and are built together. The dependency graph is just as short:
app v0.1.0 (~/ws/crates/app)
└── geometry v0.1.0 (~/ws/crates/geometry)
geometry v0.1.0 (~/ws/crates/geometry)Building the workspace produces one artifact per crate, all in the one shared target directory:
target/debug/app
target/debug/extra
target/debug/libgeometry.rlibThe binaries are executables, and libgeometry.rlib is the compiled library that app linked against.
Error one: which binary?
The package app has two binaries, and cargo run needs to know which one you mean:
cargo run -p apperror: `cargo run` could not determine which binary to run. Use the `--bin` option to specify a binary, or the `default-run` manifest key.
available binaries: app, extraThe message lists the choices and offers two fixes. Pass --bin app every time, or add default-run = "app" to the [package] table of crates/app/Cargo.toml so a plain cargo run picks one. Packages with a single binary never hit this, which is why the error only appears once you add src/bin/. See splitting a project into a library and a binary for why the library-plus-thin-binary layout is worth having.
Error two: a private item in another crate
Visibility is enforced at crate boundaries just as it is between modules. Change the import in app to reach for the private constant:
use geometry::PI_ISH as circle_area;error[E0603]: constant `PI_ISH` is private
--> crates/app/src/main.rs:2:15
|
2 | use geometry::PI_ISH as circle_area;
| ^^^^^^ private constant
|
note: the constant `PI_ISH` is defined here
--> crates/geometry/src/lib.rs:13:1
|
13 | const PI_ISH: f64 = std::f64::consts::PI;
| ^^^^^^^^^^^^^^^^^E0603: the constant is private, and the compiler shows where it is defined. Making it pub would fix the build and also make it a permanent part of the library's public interface, which is a bigger decision than it looks. Rust's rule is that if a downstream crate can use it, you have to keep supporting it. The full rules, including pub(crate), are in modules and visibility.
Which word for which job
| You want to | Use | Where it lives |
|---|---|---|
| Organise names and control visibility | a module | mod in a file, or a file/folder |
| Compile separately, or ship a library | a crate | a root file and a [lib] or [[bin]] target |
| Declare dependencies and metadata | a package | one Cargo.toml |
| Develop several packages together | a workspace | a root Cargo.toml with [workspace] |
| Publish to crates.io | a package | cargo publish publishes one package |
You publish packages, not crates and not workspaces, and the crate name users write in use comes from the package's library name (with - turned into _). More on the manifest is in Cargo.toml explained, and on sharing settings across members in Cargo workspaces explained.
Habits
- Start with one package and one binary; add a library crate when you have something to test or share.
- Keep
main.rsthin and put logic in the library, so tests can call it. - Use a workspace once you have two or more packages that change together.
- Share
edition,versionand common dependency versions through[workspace.package]and[workspace.dependencies]. - Put binaries under
src/bin/for small extra tools, and give the package adefault-runif it has more than one.
When this is too much structure
A single small program needs none of this: one package, one main.rs, a few modules in files. A workspace pays off when separate parts have different dependency sets or release cycles, or when you want the compiler to enforce a boundary that modules only suggest. Splitting into many small crates also has a cost, because each crate is a separate unit of compilation and incremental builds rebuild a crate and everything depending on it. If compile time is the problem, measure it before splitting; the cure is not always more crates.


