Series: Learn Rust from Scratch | Post 11 of 11
Introduction
Welcome to the final post in the Learn Rust from Scratch series! In the previous 10 posts, we've covered variables, functions, ownership, structs, enums, error handling, collections, traits, generics, closures, and iterators.
Now it's time to learn how to organize your code as it grows. Real Rust projects split code into modules and files, and leverage thousands of third-party libraries from crates.io — Rust's package registry.
In this post we'll cover:
- The Rust module system (
mod,use,pub) - Splitting code across multiple files
- Workspaces for multi-crate projects
- Using external crates with Cargo
The Module System
Rust's module system controls how code is organized and what is visible to other parts of your program.
Defining Modules with mod
// Everything in one file (src/main.rs) for demonstration
mod greetings {
// This function is private (default) — only accessible inside this module
fn format_name(name: &str) -> String {
format!("Dear {}", name)
}
// pub makes this function accessible from outside the module
pub fn hello(name: &str) {
println!("Hello, {}!", format_name(name));
}
pub fn goodbye(name: &str) {
println!("Goodbye, {}!", name);
}
// Nested module
pub mod formal {
pub fn greet(name: &str) {
println!("Good day, {}. How do you do?", name);
}
}
}
fn main() {
// Access module contents with ::
greetings::hello("Neo");
greetings::goodbye("Neo");
// Access nested module
greetings::formal::greet("Neo");
// greetings::format_name("Neo"); // ❌ Error: private function
}
Output:
Hello, Dear Neo!
Goodbye, Neo!
Good day, Neo. How do you do?
The use Keyword — Bringing Paths into Scope
Instead of typing the full path every time, bring items into scope with use:
mod math {
pub mod geometry {
pub fn circle_area(r: f64) -> f64 {
std::f64::consts::PI * r * r
}
pub fn rectangle_area(w: f64, h: f64) -> f64 {
w * h
}
}
pub mod stats {
pub fn mean(data: &[f64]) -> f64 {
data.iter().sum::<f64>() / data.len() as f64
}
}
}
// Bring items into scope
use math::geometry::{circle_area, rectangle_area};
use math::stats::mean;
fn main() {
println!("Circle area (r=5): {:.4}", circle_area(5.0));
println!("Rectangle (4x6): {:.4}", rectangle_area(4.0, 6.0));
let scores = vec![85.0, 92.0, 78.0, 96.0, 61.0];
println!("Mean score: {:.2}", mean(&scores));
}
Output:
Circle area (r=5): 78.5398
Rectangle (4x6): 24.0000
Mean score: 82.40
The as Keyword — Renaming Imports
use std::collections::HashMap as Map;
use std::collections::HashSet as Set;
fn main() {
let mut map: Map<&str, i32> = Map::new();
map.insert("one", 1);
map.insert("two", 2);
println!("{:?}", map);
let mut set: Set<i32> = Set::new();
set.insert(1);
set.insert(2);
println!("{:?}", set);
}
Visibility Rules with pub
mod library {
// Private struct — can't be created outside this module
struct PrivateBook {
title: String,
}
// Public struct — but fields are still private by default!
pub struct PublicBook {
pub title: String, // pub field — accessible from outside
pub author: String, // pub field
page_count: u32, // private field — only accessible inside module
}
impl PublicBook {
// Public constructor — the only way to create a PublicBook from outside
pub fn new(title: &str, author: &str, pages: u32) -> Self {
PublicBook {
title: title.to_string(),
author: author.to_string(),
page_count: pages,
}
}
// Public method to access the private field
pub fn pages(&self) -> u32 {
self.page_count
}
}
}
use library::PublicBook;
fn main() {
let book = PublicBook::new("The Rust Programming Language", "Steve Klabnik", 560);
println!("Title: {}", book.title);
println!("Author: {}", book.author);
println!("Pages: {}", book.pages()); // Through the public method
// book.page_count // ❌ Error: field is private
}
Output:
Title: The Rust Programming Language
Author: Steve Klabnik
Pages: 560
Splitting Code Across Multiple Files
As projects grow, you'll split modules into separate files.
Project Structure
my_project/
├── Cargo.toml
└── src/
├── main.rs ← entry point
├── math.rs ← math module
└── utils/
├── mod.rs ← utils module root
├── strings.rs ← utils::strings submodule
└── numbers.rs ← utils::numbers submodule
src/math.rs
// src/math.rs — this file IS the `math` module
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
pub fn factorial(n: u64) -> u64 {
(1..=n).product()
}
src/utils/mod.rs
// src/utils/mod.rs — root of the `utils` module
pub mod strings; // Declares the strings submodule
pub mod numbers; // Declares the numbers submodule
src/utils/strings.rs
// src/utils/strings.rs
pub fn capitalize(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
pub fn word_count(s: &str) -> usize {
s.split_whitespace().count()
}
src/utils/numbers.rs
// src/utils/numbers.rs
pub fn is_prime(n: u32) -> bool {
if n < 2 { return false; }
if n == 2 { return true; }
if n % 2 == 0 { return false; }
let mut i = 3;
while i * i <= n {
if n % i == 0 { return false; }
i += 2;
}
true
}
pub fn clamp(value: i32, min: i32, max: i32) -> i32 {
value.max(min).min(max)
}
src/main.rs
// src/main.rs
mod math; // Load src/math.rs
mod utils; // Load src/utils/mod.rs
use math::{add, factorial};
use utils::strings::{capitalize, word_count};
use utils::numbers::{is_prime, clamp};
fn main() {
println!("--- Math ---");
println!("3 + 4 = {}", add(3, 4));
println!("5! = {}", factorial(5));
println!("\n--- Strings ---");
println!("{}", capitalize("hello, world!"));
println!("Words in 'hello world rust': {}", word_count("hello world rust"));
println!("\n--- Numbers ---");
println!("Is 17 prime? {}", is_prime(17));
println!("Is 18 prime? {}", is_prime(18));
println!("clamp(150, 0, 100) = {}", clamp(150, 0, 100));
}
Output:
--- Math ---
3 + 4 = 7
5! = 120
--- Strings ---
Hello, world!
Words in 'hello world rust': 3
--- Numbers ---
Is 17 prime? true
Is 18 prime? false
clamp(150, 0, 100) = 100
Cargo.toml In Depth
Every Rust project has a Cargo.toml that defines metadata and dependencies:
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"
authors = ["Neo <[email protected]>"]
description = "A demo Rust project"
[dependencies]
# External crates go here — format: name = "version"
serde = { version = "1.0", features = ["derive"] } # Serialization
serde_json = "1.0" # JSON support
rand = "0.8" # Random numbers
reqwest = { version = "0.11", features = ["json"] } # HTTP client
[dev-dependencies]
# Only used for tests and benchmarks
pretty_assertions = "1.0"
[profile.release]
# Optimizations for release builds
opt-level = 3 # Maximum optimization
lto = true # Link-time optimization
Using External Crates
Rust's package ecosystem lives at crates.io. Let's use two popular crates.
Example 1: rand — Random Numbers
Add to Cargo.toml:
[dependencies]
rand = "0.8"
use rand::Rng;
fn main() {
let mut rng = rand::thread_rng();
// Random integer in a range
let roll: u32 = rng.gen_range(1..=6);
println!("Dice roll: {}", roll);
// Random float
let x: f64 = rng.gen(); // 0.0 to 1.0
println!("Random float: {:.4}", x);
// Random bool
let coin: bool = rng.gen();
println!("Coin flip: {}", if coin { "Heads" } else { "Tails" });
// Shuffle a vec
let mut deck: Vec<u32> = (1..=10).collect();
use rand::seq::SliceRandom;
deck.shuffle(&mut rng);
println!("Shuffled: {:?}", deck);
}
Example 2: serde + serde_json — JSON Serialization
Add to Cargo.toml:
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
use serde::{Deserialize, Serialize};
// Derive Serialize and Deserialize for your struct
#[derive(Debug, Serialize, Deserialize)]
struct Student {
name: String,
age: u32,
grades: Vec<f64>,
active: bool,
}
fn main() {
let student = Student {
name: String::from("Neo"),
age: 25,
grades: vec![92.5, 88.0, 95.5],
active: true,
};
// Serialize to JSON string
let json = serde_json::to_string_pretty(&student).unwrap();
println!("JSON:\n{}", json);
// Deserialize from JSON string
let json_input = r#"{
"name": "Alice",
"age": 22,
"grades": [85.0, 90.5, 78.0],
"active": false
}"#;
let alice: Student = serde_json::from_str(json_input).unwrap();
println!("\nDeserialized: {:?}", alice);
println!("Name: {}, Avg grade: {:.2}", alice.name,
alice.grades.iter().sum::<f64>() / alice.grades.len() as f64);
}
Output:
JSON:
{
"name": "Neo",
"age": 25,
"grades": [92.5, 88.0, 95.5],
"active": true
}
Deserialized: Student { name: "Alice", age: 22, grades: [85.0, 90.5, 78.0], active: false }
Name: Alice, Avg grade: 84.50
Writing Tests in Rust
Rust has built-in test support — no extra framework needed.
// src/math.rs
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn divide(a: f64, b: f64) -> Option<f64> {
if b == 0.0 { None } else { Some(a / b) }
}
// Tests go in a #[cfg(test)] module — only compiled when testing
#[cfg(test)]
mod tests {
use super::*; // Import everything from the parent module
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
assert_eq!(add(-1, 1), 0);
assert_eq!(add(0, 0), 0);
}
#[test]
fn test_divide_normal() {
let result = divide(10.0, 2.0);
assert_eq!(result, Some(5.0));
}
#[test]
fn test_divide_by_zero() {
let result = divide(10.0, 0.0);
assert_eq!(result, None);
}
#[test]
fn test_add_negative() {
assert!(add(-5, -5) < 0, "Sum of negatives should be negative");
}
#[test]
#[should_panic] // This test passes only if it panics
fn test_panic_on_bad_index() {
let v = vec![1, 2, 3];
let _ = v[10]; // Out of bounds — will panic
}
}
Run tests with:
cargo test
Output:
running 5 tests
test tests::test_add ... ok
test tests::test_add_negative ... ok
test tests::test_divide_by_zero ... ok
test tests::test_divide_normal ... ok
test tests::test_panic_on_bad_index ... ok
test result: ok. 5 passed; 0 failed; 0 ignored
Cargo Workspaces — Multiple Crates in One Project
For larger projects, a workspace groups multiple crates that share a Cargo.lock:
my_workspace/
├── Cargo.toml ← Workspace root
├── core_lib/
│ ├── Cargo.toml
│ └── src/lib.rs
└── cli_app/
├── Cargo.toml
└── src/main.rs
Workspace Cargo.toml:
[workspace]
members = [
"core_lib",
"cli_app",
]
core_lib/Cargo.toml:
[package]
name = "core_lib"
version = "0.1.0"
edition = "2021"
cli_app/Cargo.toml:
[package]
name = "cli_app"
version = "0.1.0"
edition = "2021"
[dependencies]
core_lib = { path = "../core_lib" } # Reference local crate
Complete Cargo Commands Reference
# Project management
cargo new my_project # New binary project
cargo new my_lib --lib # New library project
cargo init # Initialize in current directory
# Building and running
cargo build # Debug build
cargo build --release # Optimized release build
cargo run # Build and run
cargo run --release # Run with optimizations
cargo run -- arg1 arg2 # Pass arguments to your program
# Checking and testing
cargo check # Fast type-check without compiling binary
cargo test # Run all tests
cargo test test_name # Run a specific test
cargo test -- --nocapture # Show println! output in tests
# Code quality
cargo fmt # Auto-format code
cargo clippy # Linting — suggests improvements
cargo clippy -- -D warnings # Treat warnings as errors
# Dependencies
cargo add serde # Add a dependency (requires cargo-edit)
cargo update # Update dependencies
cargo tree # Show dependency tree
# Documentation
cargo doc # Build docs for your project
cargo doc --open # Build and open docs in browser
# Publishing
cargo login # Log in to crates.io
cargo publish # Publish your crate to crates.io
What to Learn Next
Congratulations — you've completed the Learn Rust from Scratch series! Here's what to explore next:
| Topic | What You'll Learn |
|---|---|
| Lifetimes | Tell the compiler how long references should live |
| Smart Pointers | Box<T>, Rc<T>, RefCell<T> — advanced memory patterns |
| Concurrency | Threads, Arc<Mutex<T>>, channels |
| Async/Await | Asynchronous programming with tokio or async-std |
| Macros | Write code that writes code (macro_rules!, proc macros) |
| WebAssembly | Compile Rust to WASM for the browser |
| Embedded | Run Rust on microcontrollers without an OS |
Recommended Resources
- The Rust Book — doc.rust-lang.org/book (free, comprehensive)
- Rustlings — github.com/rust-lang/rustlings (small exercises)
- Rust by Example — doc.rust-lang.org/rust-by-example
- crates.io — crates.io (package registry)
Series Complete! What We Covered
Here's a summary of all 11 posts:
| Post | Topic |
|---|---|
| 1 | Introduction to Rust — installation, Hello World, Cargo |
| 2 | Variables, Data Types, and Mutability |
| 3 | Functions and Control Flow |
| 4 | Ownership, Borrowing, and References |
| 5 | Structs and Methods |
| 6 | Enums and Pattern Matching |
| 7 | Error Handling with Result and Option |
| 8 | Collections: Vec, HashMap, HashSet |
| 9 | Traits and Generics |
| 10 | Closures and Iterators |
| 11 | Modules, Packages, and Cargo ← You are here |
Thank you for following the Learn Rust from Scratch series on CodeWithNeo. Happy coding, and may your programs always compile on the first try!


Leave a Reply