In the previous tutorial, we learned Rust collections. As projects grow, putting everything in one file becomes unmanageable. In this tutorial, we learn modules – Rust’s system for organizing code into logical units.
Good project structure makes code easier to read, test, and maintain. Rust’s module system is simple once you understand the rules. Let us learn them step by step.
What is a Module?
A module is a named container for functions, structs, enums, and other items. Modules control:
- Organization – group related code together
- Visibility – decide what is public and what is private
- Namespacing – avoid name conflicts between different parts of your code
Inline Modules
The simplest module is defined inline with the mod keyword:
mod math {
pub fn add(a: i64, b: i64) -> i64 {
a + b
}
fn internal_helper() -> i64 {
42 // This is private — only accessible inside the math module
}
}
fn main() {
println!("{}", math::add(2, 3)); // OK: add is pub
// println!("{}", math::internal_helper()); // ERROR: private
}
By default, everything in a module is private. Add pub to make items accessible from outside the module.
Visibility Rules
mod models {
pub struct User {
pub name: String, // Public field
pub email: String, // Public field
age: u32, // Private field
}
impl User {
pub fn new(name: &str, email: &str, age: u32) -> Self {
Self {
name: name.to_string(),
email: email.to_string(),
age,
}
}
pub fn age(&self) -> u32 {
self.age // Public getter for private field
}
}
}
fn main() {
let user = models::User::new("Alex", "alex@example.com", 25);
println!("{}", user.name); // OK: public field
println!("{}", user.age()); // OK: public method
// println!("{}", user.age); // ERROR: private field
}
Visibility levels:
- No keyword – private to the current module
pub– public to everyonepub(crate)– public within the crate, but not to external userspub(super)– public to the parent module only
Why Private Fields Matter
Private fields enforce invariants. If age is private, the only way to set it is through User::new(), where you can add validation:
pub fn new(name: &str, email: &str, age: u32) -> Result<Self, String> {
if age > 150 {
return Err("Age must be 150 or less".to_string());
}
Ok(Self { name: name.to_string(), email: email.to_string(), age })
}
No code outside the module can set age to an invalid value.
The use Keyword
use brings items into scope so you do not need the full path every time:
mod models {
pub struct User { pub name: String }
pub struct Task { pub title: String }
}
use models::{User, Task};
fn main() {
let _user = User { name: "Alex".to_string() };
let _task = Task { title: "Test".to_string() };
}
Common use Patterns
// Import one item
use models::User;
// Import multiple items
use models::{User, Task};
// Import everything (use sparingly)
use models::*;
// Rename to avoid conflicts
use models::User as AppUser;
// Re-export — make an item available from a different path
pub use models::User;
File-Based Modules
As your project grows, you move modules to separate files. Rust has simple rules for this.
Project Structure
src/
├── main.rs (or lib.rs for libraries)
├── math.rs (the math module)
├── models.rs (the models module)
└── utils/
├── mod.rs (declares sub-modules)
├── strings.rs
└── validation.rs
Declaring Modules
In src/main.rs (or src/lib.rs):
mod math; // Looks for src/math.rs
mod models; // Looks for src/models.rs
mod utils; // Looks for src/utils/mod.rs (because it is a directory)
fn main() {
println!("{}", math::add(2, 3));
}
This tells Rust to look for each module in either:
src/math.rs– a single filesrc/math/mod.rs– a directory with sub-modules
The Module File
src/math.rs contains the actual code:
pub fn add(a: i64, b: i64) -> i64 {
a + b
}
pub fn multiply(a: i64, b: i64) -> i64 {
a * b
}
pub fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
Notice: no mod keyword in the file itself. The file is the module. The mod declaration in the parent file connects them.
Directory Modules
For src/utils/mod.rs:
pub mod strings;
pub mod validation;
Each pub mod strings; tells Rust to look for src/utils/strings.rs.
Re-Exports
Re-exports let users import from a simpler path. In src/lib.rs:
pub mod math;
pub mod models;
// Re-exports — users can import directly from the crate root
pub use models::User;
pub use models::Task;
Without re-exports:
use my_crate::models::User;
With re-exports:
use my_crate::User;
Re-exports make your public API cleaner. Users do not need to know your internal module structure.
Nested Modules
Modules can contain other modules:
mod config {
pub struct AppConfig {
pub app_name: String,
pub debug: bool,
}
impl AppConfig {
pub fn new() -> Self {
Self {
app_name: "My App".to_string(),
debug: false,
}
}
}
pub mod defaults {
pub const MAX_RETRIES: u32 = 3;
pub const TIMEOUT_SECS: u64 = 30;
}
}
fn main() {
let config = config::AppConfig::new();
println!("App: {}", config.app_name);
println!("Max retries: {}", config::defaults::MAX_RETRIES);
}
Accessing Parent Items with super
mod services {
use super::config::AppConfig;
pub struct Logger {
prefix: String,
}
impl Logger {
pub fn new(config: &AppConfig) -> Self {
Self {
prefix: config.app_name.clone(),
}
}
pub fn log(&self, message: &str) -> String {
format!("[{}] {}", self.prefix, message)
}
}
}
super refers to the parent module. super::config accesses the config module that is a sibling of services.
lib.rs vs main.rs
A Rust project can have both:
src/lib.rs– library code (reusable)src/main.rs– binary code (the application)
src/
├── lib.rs (library — defines modules, exports public API)
├── main.rs (binary — uses the library)
└── math.rs
The binary imports from the library using the crate name:
// In src/main.rs
use my_project::math;
fn main() {
println!("{}", math::add(2, 3));
}
The crate name comes from Cargo.toml (with hyphens converted to underscores).
Using External Crates
Add dependencies in Cargo.toml:
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
rand = "0.8"
Then use them in your code:
use serde::{Serialize, Deserialize};
use rand::Rng;
#[derive(Serialize, Deserialize, Debug)]
struct Config {
name: String,
port: u16,
}
fn main() {
let mut rng = rand::thread_rng();
let port: u16 = rng.gen_range(3000..9000);
println!("Random port: {}", port);
}
Find crates at crates.io. Check download counts and recent updates to pick well-maintained crates.
Cargo Workspace
For large projects, use a workspace with multiple crates:
# Cargo.toml at the workspace root
[workspace]
members = [
"core",
"api",
"cli",
]
my-project/
├── Cargo.toml (workspace)
├── core/
│ ├── Cargo.toml (library crate)
│ └── src/lib.rs
├── api/
│ ├── Cargo.toml (binary crate)
│ └── src/main.rs
└── cli/
├── Cargo.toml (binary crate)
└── src/main.rs
Each member is an independent crate that can depend on others:
# api/Cargo.toml
[dependencies]
core = { path = "../core" }
Benefits of workspaces:
- Share a single
targetdirectory (faster builds) - Share a single
Cargo.lock(consistent dependencies) - Test everything with
cargo test --workspace
Essential Cargo Tools
# Format your code
cargo fmt
# Lint your code (find common mistakes)
cargo clippy
# Generate documentation
cargo doc --open
# Check for security vulnerabilities in dependencies
cargo audit
# Build for release (optimized)
cargo build --release
These tools are part of the Rust toolchain. Use cargo fmt and cargo clippy on every project.
Testing Modules
Each module can have its own tests:
// In src/math.rs
pub fn add(a: i64, b: i64) -> i64 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
}
}
use super::* imports everything from the parent module. Tests have access to private functions too, since they are defined inside the module.
Run tests:
cargo test # All tests
cargo test math # Tests in the math module
Common Mistakes
Mistake 1: Forgetting pub
mod math {
fn add(a: i64, b: i64) -> i64 { a + b } // Private!
}
fn main() {
// math::add(2, 3); // ERROR: function is private
}
Mistake 2: Missing mod Declaration
Creating a file is not enough. You must declare the module:
// src/main.rs
// WRONG: just having src/math.rs is not enough
// RIGHT: you need this line:
mod math;
Mistake 3: Circular Dependencies
// models.rs uses something from services.rs
// services.rs uses something from models.rs
// This creates a circular dependency!
Fix it by separating shared types into their own module that both can depend on.
Module Organization Guidelines
When to Create a New Module
- When a file grows beyond 200-300 lines
- When functions are logically related (all validation, all database queries)
- When you want to control visibility (hide implementation details)
Naming Conventions
- Module names:
snake_case(e.g.,user_service,http_client) - File names match module names:
user_service.rs - Directories for modules with sub-modules:
user_service/mod.rs
Common Project Layout
src/
├── lib.rs (or main.rs)
├── models/ (data structures)
│ ├── mod.rs
│ ├── user.rs
│ └── task.rs
├── services/ (business logic)
│ ├── mod.rs
│ └── user_service.rs
├── handlers/ (API handlers)
│ ├── mod.rs
│ └── user_handler.rs
└── db/ (database layer)
├── mod.rs
└── queries.rs
Summary
| Concept | Syntax | Purpose |
|---|---|---|
| Inline module | mod name { } | Group code in the same file |
| File module | mod name; | Load code from a file |
| Public | pub | Make items accessible |
| Crate-public | pub(crate) | Accessible within crate only |
| Import | use path::Item | Bring items into scope |
| Re-export | pub use path::Item | Expose items from a shorter path |
| Parent access | super:: | Access parent module |
| Crate root | crate:: | Access from crate root |
| Workspace | [workspace] | Multiple crates in one project |
Source Code
Related Articles
- Rust Tutorial #16: Collections – previous tutorial
- Rust Tutorial #18: Testing in Rust – next tutorial
- Rust Tutorial Series – all tutorials
What’s Next?
We now know how to organize Rust projects with modules, control visibility, use external crates, and set up workspaces. Next, we learn testing in Rust – unit tests, integration tests, assertions, and test organization. Rust has testing built into the language, and it is one of the best testing experiences in any programming language.