Crate Layout Best Practices: lib.rs
, mod.rs
, and src/bin
Rust has rapidly become one of the most-loved programming languages, thanks to its focus on safety, performance, and developer productivity. But as your Rust projects scale up, structuring your code for clarity and maintainability becomes crucial. A good project layout not only helps you stay organized but also makes onboarding new contributors effortless.
In this post, we’ll dive deep into best practices for structuring Rust crates. Whether you’re building a library, a multi-binary application, or a combination of both, we’ll cover everything you need to know about lib.rs
, mod.rs
, and src/bin
. By the end, you’ll feel confident about structuring your Rust projects for scalability and clarity.
Why Does Crate Layout Matter?
Imagine walking into a messy workshop, where you have tools scattered everywhere. It’s hard to find what you need, and collaborating with others becomes a nightmare. A poorly structured codebase is no different. Without a clear, consistent layout, navigating code feels like searching for a needle in a haystack.
Rust encourages modularity—breaking your code into smaller, reusable pieces. But modularity alone isn’t enough; you need a layout that scales well for both small and large projects. Let’s explore the building blocks of a well-structured Rust crate.
The Basics: src/lib.rs
and src/main.rs
Rust projects typically fall into one of two categories: libraries or binaries. Libraries provide reusable functionality, while binaries are standalone executables. The entry points for these are src/lib.rs
and src/main.rs
, respectively.
Libraries: src/lib.rs
For library crates, src/lib.rs
serves as the entry point. Think of it as the brain of your library—it ties everything together and exposes public functionality. A clean lib.rs
ensures your library is easy to use and understand.
Here’s a minimal library structure:
my_library/
├── src/
│ ├── lib.rs
│ ├── utils.rs
│ ├── math/
│ │ ├── mod.rs
│ │ ├── arithmetic.rs
│ │ └── geometry.rs
├── Cargo.toml
And the corresponding lib.rs
might look like this:
pub mod utils;
pub mod math;
pub use math::arithmetic::add; // Re-export for convenience
pub use math::geometry::calculate_area;
Binaries: src/main.rs
For binary crates, src/main.rs
is the entry point. It’s where your program starts execution. A clean main.rs
focuses on high-level orchestration, delegating detailed functionality to modules.
Here’s a simple binary structure:
my_binary/
├── src/
│ ├── main.rs
│ ├── config.rs
│ └── handlers.rs
├── Cargo.toml
And the main.rs
:
mod config;
mod handlers;
fn main() {
let settings = config::load();
handlers::run(settings);
}
Organizing Modules: When to Use mod.rs
When your project grows, splitting functionality across multiple files can make your code more readable and maintainable. Rust allows you to organize modules using either flat files or directories. This is where mod.rs
comes in.
Flat File Structure vs. Directory-Based Structure
A flat file structure works well for small projects:
src/
├── lib.rs
├── utils.rs
├── math.rs
But as your project grows, grouping related files into directories improves organization:
src/
├── lib.rs
├── utils.rs
├── math/
│ ├── mod.rs
│ ├── arithmetic.rs
│ └── geometry.rs
In directory-based structures, mod.rs
acts as the entry point for the module. Think of it as the front desk that directs visitors (other parts of your code) to the right file.
Example: src/math/mod.rs
pub mod arithmetic;
pub mod geometry;
pub use arithmetic::add; // Re-export for external access
pub use geometry::calculate_area;
Here’s how lib.rs
would use this module:
pub mod math;
fn main() {
let sum = math::add(2, 3);
let area = math::calculate_area(5.0);
println!("Sum: {}, Area: {}", sum, area);
}
Multi-Binary Projects: The src/bin
Directory
Sometimes you’ll need a project with multiple binaries—for example, a CLI tool with subcommands or related utilities. Rust makes this easy with the src/bin
directory. Each file in src/bin
corresponds to a separate binary.
Example Structure
my_project/
├── src/
│ ├── lib.rs
│ ├── bin/
│ │ ├── cli.rs
│ │ ├── daemon.rs
│ │ └── worker.rs
├── Cargo.toml
Each binary can import shared functionality from lib.rs
:
src/bin/cli.rs
:
use my_project::utils;
fn main() {
println!("Running CLI tool...");
utils::do_work();
}
src/bin/daemon.rs
:
use my_project::utils;
fn main() {
println!("Running daemon...");
utils::do_work();
}
This layout keeps your binaries modular while allowing them to share common functionality from the library.
Common Pitfalls (and How to Avoid Them)
1. Overusing mod.rs
While mod.rs
is useful for organizing directories, overuse can make your project harder to navigate. If your module only has one file, consider using a flat file structure instead of a directory with mod.rs
.
Bad:
src/
├── math/
│ ├── mod.rs
Better:
src/
├── math.rs
2. Exposing Too Much
Avoid exposing internal implementation details in your lib.rs
. Use pub(crate)
or avoid pub
altogether for functionality that’s only meant to be used within your crate.
Bad:
pub mod internal_utils; // This shouldn't be public
Better:
mod internal_utils; // Keep it private
3. Messy Binaries
If you’re working on multi-binary projects, don’t cram too much logic into each binary file. Delegate shared functionality to lib.rs
or separate modules.
Key Takeaways
-
Libraries: Use
lib.rs
as the entry point and organize related functionality into modules. -
Binaries: Use
main.rs
for the high-level orchestration of your binary crates. -
Multi-Binary Projects: Use the
src/bin
directory to keep binaries modular while sharing functionality vialib.rs
. -
Modules: Use
mod.rs
for directory-based modules, but don’t overuse it. - Avoid Pitfalls: Keep internal details private, avoid messy binary files, and don’t overcomplicate your layout.
Next Steps
To solidify your understanding, here’s what you can do:
- Refactor one of your existing Rust projects using the best practices outlined here.
- Explore popular Rust libraries and study their layouts (e.g., tokio).
- Dive deeper into Rust’s module system by reading The Rust Programming Language.
Remember, a clean crate layout is like a well-organized bookshelf—it makes finding what you need easy and ensures your codebase remains approachable for years to come. Happy coding! 🚀
Top comments (0)