Project Structure
Rust's module system controls how code is organized, what is public, and how names are resolved. It is different from most languages — there is no filesystem-based auto-discovery. You explicitly declare your module tree, and the compiler enforces visibility rules at compile time.
The Basics: src/main.rs & src/lib.rs
Every Rust project starts with one of two root files:
src/main.rs— binary crate, produces an executablesrc/lib.rs— library crate, produces a reusable library
A project can have both. When it does, src/main.rs can use the library crate by importing it with the crate name from Cargo.toml:
my-project/
Cargo.toml
src/
main.rs # binary crate root
lib.rs # library crate root
// src/lib.rs
pub fn process(input: &str) -> String {
input.to_uppercase()
}
// src/main.rs
use my_project::process;
fn main() {
println!("{}", process("hello"));
}
This split is common: put all logic in lib.rs (and its modules), keep main.rs thin. This lets you test the library crate independently and reuse it from other binaries.
Modules as Files vs Directories
There are two ways to define a module in a separate file.
Single-file module
src/
lib.rs
config.rs
// src/lib.rs
mod config; // loads src/config.rs
pub use config::Settings;
// src/config.rs
pub struct Settings {
pub port: u16,
pub host: String,
}
Directory module
When a module has submodules, use a directory with a mod.rs file:
src/
lib.rs
db/
mod.rs
connection.rs
query.rs
// src/lib.rs
mod db;
// src/db/mod.rs
mod connection;
mod query;
pub use connection::Pool;
pub use query::execute;
Alternatively, since Rust 2018, you can use a file named after the directory instead of mod.rs:
src/
lib.rs
db.rs # same role as db/mod.rs
db/
connection.rs
query.rs
Both approaches work. The db.rs style avoids multiple mod.rs tabs in your editor. Pick one and stay consistent.
mod, pub & use
mod declares a module
mod auth; // declares the auth module, loads from auth.rs or auth/mod.rs
mod auth { // inline module — rarely used except in tests
pub fn login() {}
}
pub controls visibility
By default, everything is private. pub makes it visible to the parent module and beyond:
mod server {
pub struct Config {
pub port: u16,
host: String, // private — only accessible within this module
}
impl Config {
pub fn new(port: u16, host: String) -> Self {
Config { port, host }
}
pub fn host(&self) -> &str {
&self.host
}
}
}
fn main() {
let config = server::Config::new(8080, "localhost".into());
println!("Port: {}", config.port);
// println!("{}", config.host); // ERROR: host is private
println!("Host: {}", config.host()); // OK: public method
}
pub(crate) makes something visible within the crate but not to external consumers — useful for internal helpers:
pub(crate) fn internal_helper() -> u32 {
42
}
use brings names into scope
use std::collections::HashMap;
use std::io::{self, Read, Write}; // grouped imports
use crate::db::Pool; // absolute path from crate root
use super::config::Settings; // relative path from parent module
Re-exports with pub use
pub use is how you curate your crate's public API. Internal organization can be deep; the public surface should be flat:
// src/lib.rs
mod domain;
mod infrastructure;
// Users of your crate see these at the top level
pub use domain::user::User;
pub use domain::order::Order;
pub use infrastructure::db::connect;
Now consumers write use your_crate::User instead of use your_crate::domain::user::User. This is critical for library crates — your module structure is an implementation detail, not an API.
The Module Tree
Rust's module system forms a tree rooted at lib.rs or main.rs. Every module has exactly one parent (except the root). The compiler resolves paths by walking this tree:
crate (lib.rs)
├── domain
│ ├── user
│ └── order
├── infrastructure
│ ├── db
│ └── cache
└── application
└── handlers
Path resolution:
crate::domain::user::User— absolute from rootself::helper()— current modulesuper::other_module::func()— parent module
Organizing a Real Project
A small project can get away with a flat module structure. Once a project grows past a few thousand lines, consider organizing by domain layer:
src/
lib.rs
main.rs
domain/
mod.rs
user.rs
order.rs
error.rs
infrastructure/
mod.rs
db.rs
cache.rs
email.rs
application/
mod.rs
handlers.rs
middleware.rs
config.rs
// src/lib.rs
pub mod domain;
pub mod infrastructure;
pub mod application;
pub mod config;
// Re-export the most-used types
pub use config::AppConfig;
pub use domain::error::AppError;
// src/domain/mod.rs
pub mod user;
pub mod order;
pub mod error;
Domain modules contain business types and validation. Error modules define your crate's error enum with Display and Error implementations.
Guidelines for layer organization
domain/— business logic, types, validation. No dependencies on infrastructure.infrastructure/— database, HTTP clients, file system, external services.application/— glue code that wires domain logic to infrastructure. Handlers, middleware, orchestration.config.rs— configuration parsing, environment variables.
This is not a hard rule — it is a starting point. The key principle is that domain code should not depend on infrastructure code. Dependencies flow inward.
Common Pitfalls
- Forgetting
moddeclarations — creating a file does not add it to the module tree. You must declaremod filename;in the parent module. - Circular dependencies — Rust does not allow circular module dependencies. If A needs B and B needs A, extract the shared types into a third module.
- Over-nesting — five levels of nested modules makes paths painful. Flatten with
pub use. - Everything
pub— making everything public defeats the purpose of the module system. Start private, expose only what consumers need. - Confusing
mod.rswithlib.rs—mod.rsis the root of a sub-module, not a crate root. It does not get special treatment from Cargo.
Key Takeaways
src/main.rsis the binary root,src/lib.rsis the library root. Use both to keepmain.rsthin.moddeclares modules.pubcontrols visibility.usebrings names into scope.pub usere-exports create a clean public API independent of internal structure.- Organize by domain layer once a project grows: domain, infrastructure, application.
- Start private, expose deliberately. The module system is your tool for enforcing API boundaries.