How to Structure a Rust Project Idiomatically
When starting a new Rust project, you’re likely to encounter a question that every developer faces: How should I organize my code? While Rust provides flexibility in how you structure your project, following idiomatic practices not only makes your code easier to maintain but also ensures clarity for other developers who work on your project.
In this blog post, we’ll explore the best practices for structuring a Rust project, from organizing your modules and crates to making smart use of pub use
. You’ll learn how to properly split logic into submodules, avoid common pitfalls, and write code that feels natural to seasoned Rustaceans. By the end, you’ll have a clear roadmap for structuring your Rust projects in a way that’s both scalable and idiomatic.
Why Rust Project Structure Matters
Rust is a systems programming language designed to be safe and performant, but it also emphasizes maintainability. The way you organize your project reflects how easy it is to understand, debug, and extend over time. A clean structure helps:
- Improve readability: Developers can quickly understand the project's organization.
- Encourage modularity: Small, focused modules make testing and reusability easier.
- Avoid spaghetti code: Prevent tightly coupled logic scattered across files.
Much like how cities organize streets, districts, and landmarks for efficiency, structuring a Rust project is about creating order, so developers (and you!) can navigate it effortlessly.
Anatomy of a Rust Project
Before diving into best practices, let’s quickly review the basic components of a Rust project:
-
Crate: A Rust package that can be a binary (executable) or library.
- If your project is an application, it typically has a
main.rs
. - If your project is a shared library, it uses
lib.rs
.
- If your project is an application, it typically has a
Modules: Rust allows you to organize code into hierarchical modules. Think of modules as a way to group related functionality.
Cargo.toml: The manifest file that defines dependencies, metadata, and crate type.
A typical Rust project structure looks like this:
my_project/
├── Cargo.toml
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── module_a.rs
│ ├── module_b/
│ │ ├── mod.rs
│ │ ├── submodule.rs
Best Practices for Structuring Rust Projects
1. Start with a Clear Entry Point: main.rs
or lib.rs
Every Rust project begins with either main.rs
(for executables) or lib.rs
(for libraries). These files act as your entry point and should provide a high-level overview of your project.
Example: A Clean main.rs
mod config; // Define configuration logic
mod server; // Define server-related functionality
use config::load_config;
use server::start_server;
fn main() {
let config = load_config("config.toml").expect("Failed to load config");
start_server(config);
}
Here:
-
mod config
andmod server
declare submodules. - The
main.rs
file gives a bird’s-eye view of the program flow.
Why This Works:
By keeping main.rs
focused on high-level logic, you avoid cluttering it with implementation details. Instead, delegate functionality to modules.
2. Split Logic into Submodules
Rust’s module system allows you to break your code into smaller, logical units. A module can be represented by a single file (module.rs
) or a directory (module/
) containing multiple files.
Example: Organizing Modules in a Directory
Let’s say you’re building a web server. You might structure your project like this:
src/
├── main.rs
├── config.rs
├── server/
│ ├── mod.rs
│ ├── handler.rs
│ ├── routes.rs
In server/mod.rs
:
pub mod handler; // Expose handler module
pub mod routes; // Expose routes module
pub fn start_server() {
println!("Starting server...");
// server logic here
}
In server/handler.rs
:
pub fn handle_request(request: &str) {
println!("Handling request: {}", request);
}
In server/routes.rs
:
pub fn get_route(route: &str) -> Option<&str> {
match route {
"/" => Some("Homepage"),
"/about" => Some("About Page"),
_ => None,
}
}
Why This Works:
Grouping related functionality into folders/modules keeps your codebase organized. It also makes navigating the project intuitive—for example, all server-related logic is in the server/
folder.
3. Use pub use
for Re-Exports
As your project grows, modules can become deeply nested, making their APIs harder to use. To simplify this, you can re-export frequently used items at a higher level using pub use
.
Example: Simplifying Access with pub use
mod server {
pub mod handler {
pub fn handle_request(request: &str) {
println!("Handling request: {}", request);
}
}
pub mod routes {
pub fn get_route(route: &str) -> Option<&str> {
match route {
"/" => Some("Homepage"),
"/about" => Some("About Page"),
_ => None,
}
}
}
pub use handler::handle_request;
pub use routes::get_route;
}
Now, instead of accessing server::handler::handle_request
, you can simply write:
use server::handle_request;
fn main() {
handle_request("GET /");
}
Why This Works:
Re-exporting reduces boilerplate and improves ergonomics, especially in larger projects where users shouldn’t need to know the internal structure of your modules.
4. Separate Concerns: Logic, Configuration, and Tests
Rust projects benefit from separating concerns into distinct modules. For example:
- Logic: Core functionality goes into dedicated modules.
-
Configuration: Use a
config.rs
file for global settings. - Tests: Group integration and unit tests logically.
Example: Adding Tests
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
#[cfg(test)]
mod tests {
use super::math;
#[test]
fn test_add() {
assert_eq!(math::add(2, 3), 5);
}
}
Here, tests live in a separate mod tests
block, ensuring the production code remains clean and focused.
5. Avoid Common Pitfalls
Pitfall 1: Over-Nesting Modules
While hierarchical modules are useful, excessive nesting can make code harder to navigate. For example:
mod app {
mod backend {
mod database {
mod queries {
pub fn get_user() {}
}
}
}
}
Accessing app::backend::database::queries::get_user
is cumbersome. Instead, flatten the structure:
mod database {
pub fn get_user() {}
}
Pitfall 2: Overuse of pub
Exposing everything with pub
can lead to tight coupling. Instead, favor encapsulation:
mod utils {
pub(crate) fn internal_helper() {
// Only accessible within the crate
}
}
Pitfall 3: Monolithic Code in main.rs
Avoid cramming all functionality into main.rs
. Delegate responsibilities to modules.
Key Takeaways
-
Keep
main.rs
(orlib.rs
) focused on high-level logic and delegate functionality to modules. - Use submodules to organize related functionality logically.
-
Re-export (
pub use
) strategically to simplify your module's public API. - Separate concerns—group logic, configuration, and tests into distinct modules.
-
Avoid over-nesting and overusing
pub
to maintain clarity and encapsulation.
Next Steps for Learning
Structuring a Rust project idiomatically is just the beginning. Here’s what you can do next:
- Explore popular Rust crates like tokio or serde to see how they structure their code.
- Practice by refactoring an existing project using the techniques you learned here.
- Dive deeper into Rust’s module system by reading The Rust Programming Language or the Modules documentation.
Remember: A well-organized project is a joy to work with—not just for you but for everyone on your team. Happy coding! 🚀
Top comments (0)