Rust Modules: Code Organization and Visibility
Rust's module system is fundamentally different from what you might expect coming from other languages. Unlike Java's packages or C++'s namespaces, Rust modules serve two critical purposes...
Key Insights
- Rust’s module system serves dual purposes: organizing code into logical units and controlling visibility through privacy boundaries that are enforced at compile time.
- The file system structure directly mirrors your module hierarchy, but Rust gives you fine-grained visibility control beyond simple public/private with
pub(crate),pub(super), andpub(in path). - Effective module design means exposing a minimal public API while keeping implementation details private—use
lib.rsas your public interface and leverage re-exports to create clean, discoverable APIs.
Introduction to Rust’s Module System
Rust’s module system is fundamentally different from what you might expect coming from other languages. Unlike Java’s packages or C++’s namespaces, Rust modules serve two critical purposes simultaneously: they organize your code into logical hierarchies and enforce privacy boundaries at compile time.
This dual nature is intentional. Rust doesn’t have a separate visibility system bolted onto a namespace system. Instead, modules are the privacy boundary. Everything is private by default, and you explicitly choose what to expose. This design pushes you toward better encapsulation and makes it harder to accidentally create tight coupling between components.
The module system scales from small scripts to large applications. You can start with everything in one file using inline modules, then refactor to a file-per-module structure as your project grows, all without changing how other code imports and uses your types.
Creating and Structuring Modules
Modules in Rust are declared with the mod keyword. The simplest form is an inline module:
mod network {
fn connect() {
println!("Connecting...");
}
}
fn main() {
// This won't compile - connect() is private
// network::connect();
}
This works fine for small modules, but real projects need better organization. When you write mod network;, Rust looks for either network.rs in the same directory or network/mod.rs. Both work, but the convention has shifted toward network.rs for simplicity.
Here’s a practical file structure:
src/
├── main.rs
├── network.rs
└── database/
├── mod.rs
├── connection.rs
└── queries.rs
In main.rs:
mod network;
mod database;
fn main() {
// Use modules here
}
In network.rs:
pub fn connect(addr: &str) -> Result<Connection, Error> {
// Implementation
}
struct Connection {
// Private by default
}
For the database directory, database/mod.rs acts as the module root:
// database/mod.rs
mod connection;
mod queries;
pub use connection::Connection;
pub use queries::{find_user, create_user};
This structure lets you split related functionality across multiple files while presenting a clean interface. Users import from database, not from database::connection.
Nested modules work exactly as you’d expect:
mod network {
pub mod server {
pub fn start() {
println!("Server starting");
}
}
pub mod client {
pub fn connect() {
println!("Client connecting");
}
}
}
fn main() {
network::server::start();
}
Visibility and Privacy Rules
Rust’s default-private approach is aggressive compared to most languages. Everything—functions, structs, fields, modules—is private unless explicitly marked pub. This forces you to think about your API surface.
mod api {
pub struct User {
pub id: u64,
pub name: String,
email: String, // Private field
}
impl User {
pub fn new(id: u64, name: String, email: String) -> Self {
User { id, name, email }
}
// Private method
fn validate_email(&self) -> bool {
self.email.contains('@')
}
pub fn email(&self) -> &str {
&self.email
}
}
}
The email field is private, so external code can’t modify it directly. This is intentional—you can change internal representation without breaking external code.
Rust provides granular visibility controls beyond simple public/private:
mod parent {
pub(crate) fn crate_visible() {
// Visible anywhere in this crate
}
pub(super) fn parent_visible() {
// Visible to parent module only
}
pub(in crate::parent) fn restricted() {
// Visible within parent module tree
}
pub mod child {
pub fn test() {
super::parent_visible(); // Works
super::crate_visible(); // Works
}
}
}
Use pub(crate) liberally for internal APIs that should be accessible across your crate but not exposed to users. It’s the sweet spot between overly restrictive and overly permissive.
The use Keyword and Path Management
The use keyword brings items into scope, reducing verbosity. Paths can be absolute (starting from crate root) or relative:
// Absolute paths
use crate::network::server::Connection;
use std::collections::HashMap;
// Relative paths
use super::database::User;
use self::helpers::validate;
mod helpers {
pub fn validate(s: &str) -> bool {
!s.is_empty()
}
}
The crate:: prefix refers to your crate root, while super:: goes up one module level. Use crate:: for clarity when importing from your own modules—it makes refactoring easier.
Re-exporting with pub use is crucial for API design:
// lib.rs
mod internal {
pub mod user {
pub struct User {
pub name: String,
}
}
pub mod auth {
pub fn authenticate(token: &str) -> bool {
!token.is_empty()
}
}
}
// Flatten the API
pub use internal::user::User;
pub use internal::auth::authenticate;
Now users write use mylib::User instead of use mylib::internal::user::User. You’ve hidden implementation details and created a cleaner API.
Aliasing helps avoid naming conflicts:
use std::io::Result as IoResult;
use std::fmt::Result as FmtResult;
fn read_file() -> IoResult<String> {
// ...
}
fn format_data() -> FmtResult {
// ...
}
Organizing a Real-World Project
Let’s structure a web API project properly. Here’s a feature-based organization:
src/
├── lib.rs
├── main.rs
├── models/
│ ├── mod.rs
│ ├── user.rs
│ └── post.rs
├── handlers/
│ ├── mod.rs
│ ├── users.rs
│ └── posts.rs
├── db/
│ ├── mod.rs
│ └── pool.rs
└── middleware/
├── mod.rs
└── auth.rs
In lib.rs, expose only what external code needs:
// lib.rs
mod models;
mod handlers;
mod db;
mod middleware;
// Public API
pub use models::{User, Post};
pub use handlers::{create_user, get_user, create_post};
// Internal utilities, visible within crate
pub(crate) use db::get_pool;
pub(crate) use middleware::require_auth;
The main.rs file uses the library:
// main.rs
use myapi::{create_user, get_user};
fn main() {
// Application setup
let user = create_user("Alice");
println!("{:?}", get_user(user.id));
}
In models/mod.rs:
mod user;
mod post;
pub use user::User;
pub use post::Post;
// Keep these internal
pub(crate) use user::UserRepository;
pub(crate) use post::PostRepository;
This structure separates public types from internal implementation details. The Repository types are visible within your crate but not to external users.
Common Patterns and Best Practices
Create a prelude module for commonly used imports:
// prelude.rs
pub use crate::models::{User, Post, Comment};
pub use crate::errors::{Error, Result};
pub use crate::db::Transaction;
// Users can now write:
// use myapi::prelude::*;
Use this sparingly—only include items that are genuinely used together frequently.
Keep your public API minimal. Every public item is a commitment you’ll need to maintain:
// Good: minimal surface area
pub struct Config {
// Private fields
timeout: u64,
}
impl Config {
pub fn new(timeout: u64) -> Self {
Config { timeout }
}
pub fn timeout(&self) -> u64 {
self.timeout
}
}
// Bad: exposing internals
pub struct Config {
pub timeout: u64,
pub internal_buffer_size: usize, // Do users need this?
}
Avoid deep nesting. If you find yourself writing use crate::a::b::c::d::Thing, your module structure is probably too complex. Flatten with re-exports:
// Instead of deep nesting
mod deeply {
pub mod nested {
pub mod structure {
pub struct Thing;
}
}
}
// Flatten it
mod internal;
pub use internal::Thing;
Watch for circular dependencies. Rust prevents them at compile time, but the error messages can be confusing. If module A needs module B and vice versa, extract shared types into a third module that both depend on.
The module system is one of Rust’s strengths. Use it to create clear boundaries, hide implementation details, and design APIs that are hard to misuse. Start with everything private, then expose only what’s necessary. Your future self—and your users—will thank you.