As projects grow in size and complexity, organizing code becomes increasingly important. Rust’s module system provides a powerful framework for structuring your code into logical units, controlling visibility, and managing dependencies. Unlike some languages where organization is merely a convention, Rust’s module system is a core language feature that enforces boundaries and visibility rules at compile time, leading to more maintainable and understandable codebases.
In this comprehensive guide, we’ll explore Rust’s module system in depth, from basic concepts to advanced techniques. You’ll learn how to organize your code effectively, control what’s public and private, and create clear boundaries between components. By the end, you’ll have a solid understanding of how to leverage Rust’s module system to build well-structured, maintainable projects.
Understanding the Basics of Rust’s Module System
At its core, Rust’s module system is about organizing code and controlling visibility. Let’s start with the fundamental concepts:
Modules: Organizing Code into Logical Units
A module is a container for items such as functions, structs, traits, impl blocks, and even other modules:
// Define a module named 'geometry'
mod geometry {
// Constants within the module
const PI: f64 = 3.14159;
// Functions within the module
pub fn area_of_circle(radius: f64) -> f64 {
PI * radius * radius
}
// Nested module
pub mod shapes {
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Rectangle {
Rectangle { width, height }
}
pub fn area(&self) -> f64 {
self.width * self.height
}
}
}
}
fn main() {
// Using items from the module
let area = geometry::area_of_circle(5.0);
println!("Area of circle: {}", area);
let rect = geometry::shapes::Rectangle::new(10.0, 5.0);
println!("Area of rectangle: {}", rect.area());
}
Visibility and the pub Keyword
By default, everything in Rust is private. The pub
keyword makes items accessible outside their defining module:
mod math {
// Private function, only accessible within this module
fn add(a: i32, b: i32) -> i32 {
a + b
}
// Public function, accessible from outside the module
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
// Public function that uses a private function
pub fn add_and_multiply(a: i32, b: i32, c: i32) -> i32 {
// We can call add() here because we're in the same module
multiply(add(a, b), c)
}
}
fn main() {
// math::add(1, 2); // Error: add is private
let result = math::multiply(2, 3);
println!("2 * 3 = {}", result);
let result = math::add_and_multiply(1, 2, 3);
println!("(1 + 2) * 3 = {}", result);
}
Paths: Referring to Items in the Module Tree
Rust uses paths to refer to items in the module tree:
mod outer {
pub mod inner {
pub fn function() {
println!("This is a function in the inner module");
}
}
}
fn main() {
// Absolute path
crate::outer::inner::function();
// Relative path
outer::inner::function();
}
The use Keyword: Bringing Paths into Scope
The use
keyword brings items into scope to avoid repeating long paths:
mod shapes {
pub struct Circle {
pub radius: f64,
}
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
}
use shapes::Circle;
use shapes::Rectangle;
// Alternatively, using a single use statement:
// use shapes::{Circle, Rectangle};
fn main() {
let circle = Circle { radius: 5.0 };
let rectangle = Rectangle { width: 10.0, height: 5.0 };
println!("Circle radius: {}", circle.radius);
println!("Rectangle dimensions: {} x {}", rectangle.width, rectangle.height);
}
Module Organization in Files and Directories
As projects grow, organizing modules across files becomes essential. Rust provides several ways to structure your code:
Single-File Modules
For small projects, you can define all modules in a single file:
// src/main.rs
mod config {
pub struct Config {
pub api_key: String,
pub timeout: u32,
}
impl Config {
pub fn new(api_key: String, timeout: u32) -> Config {
Config { api_key, timeout }
}
}
}
mod api {
use super::config::Config;
pub struct Client {
config: Config,
}
impl Client {
pub fn new(config: Config) -> Client {
Client { config }
}
pub fn send_request(&self, endpoint: &str) {
println!(
"Sending request to {} with API key {} and timeout {}",
endpoint,
self.config.api_key,
self.config.timeout
);
}
}
}
use config::Config;
use api::Client;
fn main() {
let config = Config::new(String::from("my-api-key"), 30);
let client = Client::new(config);
client.send_request("/users");
}
Modules in Separate Files
For larger projects, you can split modules into separate files:
// src/main.rs
mod config; // Declares the module and tells Rust to look for its contents in another file
mod api;
use config::Config;
use api::Client;
fn main() {
let config = Config::new(String::from("my-api-key"), 30);
let client = Client::new(config);
client.send_request("/users");
}
// src/config.rs
pub struct Config {
pub api_key: String,
pub timeout: u32,
}
impl Config {
pub fn new(api_key: String, timeout: u32) -> Config {
Config { api_key, timeout }
}
}
// src/api.rs
use crate::config::Config;
pub struct Client {
config: Config,
}
impl Client {
pub fn new(config: Config) -> Client {
Client { config }
}
pub fn send_request(&self, endpoint: &str) {
println!(
"Sending request to {} with API key {} and timeout {}",
endpoint,
self.config.api_key,
self.config.timeout
);
}
}
Modules in Directories
For even more complex projects, you can organize modules in directories:
src/
├── main.rs
├── config/
│ ├── mod.rs
│ └── types.rs
└── api/
├── mod.rs
├── client.rs
└── response.rs
// src/main.rs
mod config;
mod api;
use config::Config;
use api::Client;
fn main() {
let config = Config::new(String::from("my-api-key"), 30);
let client = Client::new(config);
let response = client.send_request("/users");
println!("Response status: {}", response.status);
}
// src/config/mod.rs
mod types; // Submodule
pub use types::ConfigType;
pub struct Config {
pub api_key: String,
pub timeout: u32,
pub config_type: ConfigType,
}
impl Config {
pub fn new(api_key: String, timeout: u32) -> Config {
Config {
api_key,
timeout,
config_type: ConfigType::Production,
}
}
}
// src/config/types.rs
pub enum ConfigType {
Development,
Testing,
Production,
}
// src/api/mod.rs
mod client;
mod response;
pub use client::Client;
pub use response::Response;
// src/api/client.rs
use crate::config::Config;
use super::response::Response;
pub struct Client {
config: Config,
}
impl Client {
pub fn new(config: Config) -> Client {
Client { config }
}
pub fn send_request(&self, endpoint: &str) -> Response {
println!(
"Sending request to {} with API key {} and timeout {}",
endpoint,
self.config.api_key,
self.config.timeout
);
Response { status: 200, body: String::from("Success") }
}
}
// src/api/response.rs
pub struct Response {
pub status: u32,
pub body: String,
}
The Modern Approach: 2018 Edition Path Changes
With Rust 2018, the module system was simplified. You can now use the following structure:
src/
├── main.rs
├── config.rs
└── api/
├── mod.rs
├── client.rs
└── response.rs
// src/main.rs
mod config;
mod api;
use config::Config;
use api::Client;
fn main() {
let config = Config::new(String::from("my-api-key"), 30);
let client = Client::new(config);
let response = client.send_request("/users");
println!("Response status: {}", response.status);
}
// src/config.rs
pub struct Config {
pub api_key: String,
pub timeout: u32,
}
impl Config {
pub fn new(api_key: String, timeout: u32) -> Config {
Config { api_key, timeout }
}
}
// src/api/mod.rs
mod client;
mod response;
pub use client::Client;
pub use response::Response;
// src/api/client.rs
use crate::config::Config;
use super::response::Response;
pub struct Client {
config: Config,
}
impl Client {
pub fn new(config: Config) -> Client {
Client { config }
}
pub fn send_request(&self, endpoint: &str) -> Response {
println!(
"Sending request to {} with API key {} and timeout {}",
endpoint,
self.config.api_key,
self.config.timeout
);
Response { status: 200, body: String::from("Success") }
}
}
// src/api/response.rs
pub struct Response {
pub status: u32,
pub body: String,
}
Advanced Visibility Rules
Rust’s visibility system is more nuanced than just public and private:
Visibility Modifiers
pub
: Visible to all code- No modifier (default): Visible only within the current module and its descendants
pub(crate)
: Visible within the current cratepub(super)
: Visible within the parent modulepub(in path)
: Visible within the specified path
mod outer {
// Visible only within outer
fn private_function() {
println!("This is private to outer");
}
// Visible everywhere
pub fn public_function() {
println!("This is public");
private_function(); // Can call private_function here
}
// Visible only within the current crate
pub(crate) fn crate_visible_function() {
println!("This is visible within the crate");
}
pub mod inner {
// Visible only within outer
pub(super) fn super_visible_function() {
println!("This is visible to the parent module");
}
// Visible only within outer and its descendants
pub(in crate::outer) fn path_visible_function() {
println!("This is visible within the specified path");
}
}
}
fn main() {
outer::public_function();
outer::crate_visible_function();
// outer::private_function(); // Error: private
// outer::inner::super_visible_function(); // Error: only visible to parent
// outer::inner::path_visible_function(); // Error: only visible within path
}
Struct Field Visibility
Struct fields have their own visibility rules:
mod data {
pub struct User {
pub username: String, // Visible everywhere
pub email: String, // Visible everywhere
password: String, // Private to the module
}
impl User {
pub fn new(username: String, email: String, password: String) -> User {
User {
username,
email,
password,
}
}
pub fn check_password(&self, password: &str) -> bool {
self.password == password // Can access private field here
}
}
}
fn main() {
let user = data::User::new(
String::from("alice"),
String::from("[email protected]"),
String::from("password123"),
);
println!("Username: {}", user.username);
println!("Email: {}", user.email);
// println!("Password: {}", user.password); // Error: password is private
let is_valid = user.check_password("password123");
println!("Password valid: {}", is_valid);
}
Enum Variant Visibility
Enum variants inherit the visibility of the enum:
mod status {
pub enum Status {
Active,
Inactive,
Suspended,
}
// Even with pub(crate), all variants are still accessible
// when Status is accessible
pub(crate) enum InternalStatus {
Initializing,
Processing,
ShuttingDown,
}
}
fn main() {
let status = status::Status::Active;
match status {
status::Status::Active => println!("Active"),
status::Status::Inactive => println!("Inactive"),
status::Status::Suspended => println!("Suspended"),
}
// We can access InternalStatus because it's pub(crate)
let internal = status::InternalStatus::Processing;
}
Re-exporting and the Facade Pattern
Re-exporting allows you to create a public API that’s different from your internal structure:
Basic Re-exporting
mod inner {
pub fn function() {
println!("This is a function in the inner module");
}
pub struct Data {
pub value: i32,
}
}
// Re-export inner::function as function
pub use inner::function;
// Re-export inner::Data as Data
pub use inner::Data;
fn main() {
// Now we can call function directly
function();
// And use Data directly
let data = Data { value: 42 };
println!("Data value: {}", data.value);
}
The Facade Pattern
The facade pattern creates a simplified public API by re-exporting selected items:
// lib.rs
mod config;
mod api;
mod database;
mod utils;
// Public API
pub use config::Config;
pub use api::Client;
pub use api::Response;
// These modules and their contents remain private
// database::*
// utils::*
Users of your library only see the re-exported items, creating a clean, focused API.
Managing Dependencies with the use Keyword
The use
keyword helps manage dependencies and reduce repetition:
Basic use Statements
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn subtract(a: i32, b: i32) -> i32 {
a - b
}
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
}
// Bring individual functions into scope
use math::add;
use math::subtract;
// Or use a grouped import
use math::{add, subtract};
// Or bring everything into scope (not recommended for large modules)
// use math::*;
fn main() {
println!("2 + 3 = {}", add(2, 3));
println!("5 - 2 = {}", subtract(5, 2));
println!("4 * 5 = {}", math::multiply(4, 5)); // Not imported, so we use the full path
}
Renaming with as
use std::io::Result as IoResult;
use std::fmt::Result as FmtResult;
fn io_function() -> IoResult<()> {
// IO operation
Ok(())
}
fn fmt_function() -> FmtResult {
// Formatting operation
Ok(())
}
Nested Paths
// Instead of:
// use std::io;
// use std::io::Write;
// use std::io::Read;
// You can write:
use std::io::{self, Write, Read};
fn main() {
let mut buffer = io::Cursor::new(Vec::new());
buffer.write_all(b"Hello, world!").unwrap();
}
External Crate Dependencies
// Cargo.toml
// [dependencies]
// serde = { version = "1.0", features = ["derive"] }
// serde_json = "1.0"
// In your Rust file:
use serde::{Serialize, Deserialize};
use serde_json;
#[derive(Serialize, Deserialize)]
struct Person {
name: String,
age: u32,
}
fn main() {
let person = Person {
name: String::from("Alice"),
age: 30,
};
let json = serde_json::to_string(&person).unwrap();
println!("JSON: {}", json);
}
The super, self, and crate Keywords
Rust provides special keywords for referring to different parts of the module hierarchy:
self: The Current Module
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn operate(a: i32, b: i32) -> i32 {
// self refers to the current module
self::add(a, b)
}
}
super: The Parent Module
mod parent {
fn private_function() -> i32 {
42
}
pub mod child {
pub fn call_parent() -> i32 {
// super refers to the parent module
super::private_function()
}
}
}
fn main() {
println!("Result: {}", parent::child::call_parent());
}
crate: The Root of the Current Crate
mod deeply {
pub mod nested {
pub mod module {
pub fn function() {
// Absolute path from the crate root
crate::top_level_function();
}
}
}
}
fn top_level_function() {
println!("This is a top-level function");
}
fn main() {
deeply::nested::module::function();
}
Modules and Testing
Rust’s module system interacts with testing in specific ways:
Unit Tests
Unit tests are typically placed in the same file as the code they test, in a module annotated with #[cfg(test)]
:
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
// Bring the parent module's items into scope
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
Integration Tests
Integration tests are placed in a separate tests
directory at the same level as src
:
project/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
└── integration_test.rs
// tests/integration_test.rs
use my_crate; // The crate being tested
#[test]
fn test_add_integration() {
assert_eq!(my_crate::add(2, 3), 5);
}
Best Practices for Module Organization
Based on experience from large Rust projects, here are some best practices:
1. Follow the Principle of Least Visibility
Make items as private as possible:
mod database {
// Private implementation details
const CONNECTION_TIMEOUT: u32 = 30;
fn establish_connection() -> Connection {
// Implementation
}
// Public API
pub struct Connection {
// Fields are private
handle: u64,
}
impl Connection {
pub fn query(&self, sql: &str) -> Result<Vec<Row>, Error> {
// Implementation
}
}
}
2. Create a Clear Public API
Use re-exports to create a clean public interface:
// lib.rs
mod config;
mod database;
mod api;
mod utils;
// Public API
pub use config::Config;
pub use database::Connection;
pub use api::{Client, Response};
// Internal module structure remains hidden
3. Group Related Functionality
Organize related functionality into modules:
mod user {
pub struct User {
// Fields
}
pub fn create_user() -> User {
// Implementation
}
pub fn authenticate_user() -> bool {
// Implementation
}
}
mod product {
pub struct Product {
// Fields
}
pub fn list_products() -> Vec<Product> {
// Implementation
}
}
4. Use Submodules for Implementation Details
Hide implementation details in submodules:
pub mod http {
// Public API
pub struct Client {
// Fields
}
impl Client {
pub fn new() -> Client {
let config = self::config::default_config();
// Implementation
}
pub fn get(&self, url: &str) -> Response {
// Implementation
}
}
pub struct Response {
// Fields
}
// Private implementation details
mod config {
pub(super) fn default_config() -> Config {
// Implementation
}
pub(super) struct Config {
// Fields
}
}
}
5. Avoid Deep Module Hierarchies
Keep your module hierarchy relatively flat:
// Instead of:
mod a {
mod b {
mod c {
mod d {
pub fn function() {
// Implementation
}
}
}
}
}
// Prefer:
mod a_b {
pub fn function1() {
// Implementation
}
}
mod a_c {
pub fn function2() {
// Implementation
}
}
Real-World Example: A Complete Project Structure
Let’s look at a complete example of a well-structured Rust project:
my_project/
├── Cargo.toml
├── src/
│ ├── main.rs # Application entry point
│ ├── lib.rs # Library entry point and public API
│ ├── config/ # Configuration handling
│ │ ├── mod.rs
│ │ └── types.rs
│ ├── api/ # API client
│ │ ├── mod.rs
│ │ ├── client.rs
│ │ └── response.rs
│ ├── database/ # Database interaction
│ │ ├── mod.rs
│ │ ├── models.rs
│ │ └── schema.rs
│ └── utils/ # Utility functions
│ ├── mod.rs
│ └── helpers.rs
└── tests/ # Integration tests
├── api_tests.rs
└── database_tests.rs
// src/lib.rs
pub mod config;
pub mod api;
mod database; // Not public
mod utils; // Not public
// Re-export public API
pub use config::Config;
pub use api::{Client, Response};
// Public functions that use private modules
pub fn initialize() -> Result<(), Error> {
let db_config = database::get_config();
database::initialize(db_config)?;
Ok(())
}
pub fn get_data(query: &str) -> Result<Vec<String>, Error> {
let connection = database::connect()?;
let results = database::query(&connection, query)?;
Ok(results)
}
// src/main.rs
use my_project::{Config, Client, initialize, get_data};
fn main() -> Result<(), Box<dyn std::error::Error>> {
initialize()?;
let config = Config::new("config.json")?;
let client = Client::new(config);
let response = client.send_request("/api/data");
println!("Response: {:?}", response);
let data = get_data("SELECT * FROM items")?;
println!("Data: {:?}", data);
Ok(())
}
Conclusion
Rust’s module system is a powerful tool for organizing code, controlling visibility, and creating clear boundaries between components. By understanding how modules work and following best practices, you can create well-structured, maintainable projects that are easy to understand and extend.
Key takeaways from this guide include:
- Use modules to organize related code into logical units
- Control visibility with the
pub
keyword and other visibility modifiers - Structure your code across files and directories as your project grows
- Create a clear public API using re-exports
- Follow the principle of least visibility to encapsulate implementation details
- Use the module system to enforce boundaries between components
As you continue your journey with Rust, you’ll find that a well-organized module structure becomes increasingly important as your projects grow in size and complexity. The time invested in thoughtfully structuring your code will pay dividends in maintainability, readability, and collaboration with other developers.
Remember that the module system is not just about organization—it’s about communication. A well-structured codebase communicates its design and intentions to other developers (including your future self), making it easier to understand, modify, and extend. By mastering Rust’s module system, you’re taking a significant step toward writing better, more maintainable code.