Structure your code with packages, crates, and modules.
As your Rust programs grow, you need ways to organize code. Rust gives you three tools: packages, crates, and modules. Let's break them down.
By the end of this lesson, you'll understand how Rust projects are structured, how to split code across files, and why everything is private by default.
// A crate is the smallest unit of code the compiler considers
// Two types exist:
// Binary crate: has main(), compiles to executable
fn main() {
println!("I'm a binary crate!");
}
// Library crate: no main(), defines functionality for others
pub fn do_something() {
// Other crates can use this
}
A crate is the smallest unit of code the Rust compiler considers at once. Think of it as a tree of modules that produces either an executable or a library.
There are exactly two types:
main() function, compiles to an executable you can runmain(), defines functionality for other crates to useMost of the time when Rustaceans say "crate," they mean library crate. You'll hear "crate" used interchangeably with "library."
The crate root is the entry file. For binary crates, it's src/main.rs. For library crates, it's src/lib.rs. The compiler starts here and builds everything.
A package can have at most one library crate but unlimited binary crates. This is great for projects like CLI tools that share a core library.
# Cargo.toml defines a package
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
A package is a bundle of one or more crates with a Cargo.toml. When you run cargo new my_project, you get a package.
The rules are simple:
Here's a typical project structure:
my_project/
├── Cargo.toml # Package definition
├── src/
│ ├── main.rs # Default binary crate root
│ ├── lib.rs # Library crate root (optional)
│ └── bin/
│ ├── server.rs # Additional binary
│ └── client.rs # Additional binary
Each file in src/bin/ becomes a separate binary crate. Run them with cargo run --bin server or cargo run --bin client.
What file is the crate root for a library crate?
// src/main.rs
mod garden; // Declares a module, looks for src/garden.rs
fn main() {
garden::harvest();
}
// src/garden.rs
pub fn harvest() {
println!("Harvesting vegetables!");
}
Modules let you organize code within a crate. Declare them with the mod keyword.
When you write mod garden;, Rust looks for the module code in two places:
src/garden.rs (modern style, preferred)src/garden/mod.rs (older style)Both work, but prefer garden.rs over garden/mod.rs. With many modules, you'll end up with many files named mod.rs and your editor tabs become useless.
src/
├── main.rs
├── garden.rs # mod garden
└── garden/
└── vegetables.rs # mod vegetables (inside garden)
// src/main.rs
mod garden;
fn main() {
garden::vegetables::pick();
}
// src/garden.rs
pub mod vegetables; // Looks for src/garden/vegetables.rs
// src/garden/vegetables.rs
pub fn pick() {
println!("Picking vegetables!");
}
For nested modules, Rust looks in a subdirectory matching the parent module name. The file src/garden/vegetables.rs becomes the garden::vegetables module.
The older mod.rs style puts everything in garden/mod.rs instead of garden.rs. Both compile the same way, but garden.rs is cleaner. Stick with the modern style.
// src/main.rs
mod garden;
fn main() {
// This works - garden and vegetables are public
garden::vegetables::Asparagus::new();
}
// src/garden.rs
pub mod vegetables; // pub makes it accessible outside garden
// src/garden/vegetables.rs
pub struct Asparagus {
pub height: u32, // pub on each field too!
soil_type: String, // private - not accessible outside
}
impl Asparagus {
pub fn new() -> Self {
Asparagus {
height: 0,
soil_type: String::from("loam"),
}
}
}
This is crucial: everything in Rust is private by default. To expose something, add pub at EVERY level:
pub mod vegetables)pub struct Asparagus)pub height: u32)Miss any level and external code can't access it. This is intentional. Rust makes you explicitly choose what's part of your public API.
Private-by-default means you can refactor internals freely. Only public items are promises to external users. Change private code without breaking anyone.
// Absolute path - starts from crate root
crate::garden::vegetables::pick();
// Relative path - starts from current module
garden::vegetables::pick();
// Super - go up to parent module
super::sibling_function();
Two ways to reference items:
Absolute paths start with crate:: and work from the crate root. Use these when the definition and usage might move independently.
Relative paths start from the current module. Use super to go up one level, like .. in a filesystem. You can chain super to go up multiple levels: super::super:: goes up two levels.
mod back_of_house {
pub fn fix_order() {
// Go up to parent, then into front_of_house
super::front_of_house::serve_order();
}
}
mod front_of_house {
pub fn serve_order() {}
}
Choose based on what makes sense for your code. Absolute paths are longer but more explicit. Relative paths are shorter but break if you move the calling code.
// Without use - verbose
fn eat() {
crate::front_of_house::hosting::add_to_waitlist();
crate::front_of_house::hosting::seat_at_table();
crate::front_of_house::hosting::add_to_waitlist();
}
// With use - clean
use crate::front_of_house::hosting;
fn eat() {
hosting::add_to_waitlist();
hosting::seat_at_table();
hosting::add_to_waitlist();
}
The use keyword brings paths into scope so you don't repeat yourself.
There's an idiom here:
use hosting), then call hosting::add_to_waitlist(). This makes it clear the function isn't locally defined.use HashMap), then use HashMap::new().// Functions - bring parent module
use std::collections::hash_map;
let map = hash_map::HashMap::new(); // Clear it's from hash_map
// Structs/enums - bring the type
use std::collections::HashMap;
let map = HashMap::new(); // More idiomatic for types
When two types have the same name, bring their parent modules to disambiguate. You can't have two Result types in scope without qualification.
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
Ok(())
}
fn function2() -> IoResult<()> {
Ok(())
}
When names collide, rename one with as. This keeps both accessible without full paths.
// src/lib.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// Re-export for external users
pub use crate::front_of_house::hosting;
// Now external code can use:
// my_crate::hosting::add_to_waitlist()
// Instead of:
// my_crate::front_of_house::hosting::add_to_waitlist()
pub use re-exports an item at a different location. Your internal structure might be deep, but you can expose a flat public API.
This is powerful for library design. Organize internally however makes sense, then present a clean interface to users.
// Before - repetitive
use std::cmp::Ordering;
use std::io;
// After - nested
use std::{cmp::Ordering, io};
// Bring a module and something inside it
use std::io::{self, Write};
// self means std::io itself, plus std::io::Write
Nest paths with curly braces to reduce boilerplate. The self keyword refers to the path before the braces.
You can also use the glob operator to bring everything from a module into scope:
use std::collections::*;
Use glob sparingly. It makes it harder to tell where names come from.
mod back_of_house {
pub struct Breakfast {
pub toast: String, // Public - user chooses
seasonal_fruit: String, // Private - chef chooses
}
impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
pub enum Appetizer {
Soup, // Public because enum is public
Salad, // Also public
}
}
Structs and enums have different privacy rules:
pub individually. If any field is private, external code can't construct the struct directly (need a constructor function).If a struct has private fields, external code can't use struct literal syntax. You must provide a public constructor function like Breakfast::summer() above.
Here's a realistic project structure:
my_server/
├── Cargo.toml
├── src/
│ ├── main.rs # Binary: starts the server
│ ├── lib.rs # Library: shared code
│ ├── config.rs # Configuration module
│ ├── routes.rs # Route definitions
│ └── handlers/
│ ├── mod.rs # Handlers module (uses old style here)
│ ├── auth.rs # Auth handlers
│ └── api.rs # API handlers
└── tests/
└── integration.rs # Integration tests
// src/lib.rs
pub mod config;
pub mod routes;
pub mod handlers;
pub use config::Config; // Re-export for convenience
// src/main.rs
use my_server::Config;
fn main() {
let config = Config::from_env();
// Start server...
}
The binary crate (main.rs) uses the library crate (lib.rs). This lets your integration tests also use the library, and lets others depend on your project as a library.
What keyword brings an item into scope so you don't need the full path?
Rust's module system might feel strict at first, but it pays off. You get:
pub marks your public API explicitlypub use creates clean, flat APIsStart simple. One main.rs file works fine for small programs. As your code grows, split into modules. Extract a library crate when you want to share code or write integration tests.
Don't over-organize early. Begin with one file, then extract modules when you notice logical groupings. Let the code tell you where the boundaries should be.
The key principles:
Cargo.tomlpub explicitlyNext, we'll explore Rust's standard library collections: Vec, String, and HashMap. These dynamic data structures build on everything you've learned about ownership and borrowing.