Code Organization by Project Type and Size
Code Organization for a Simple Library
A simple library crate may consist of its crate root (e.g. the lib.rs
file) and several modules in separate files.
- src
- lib.rs
- module1.rs
- module2.rs
- ...
Cargo.toml
where lib.rs
includes the modules with mod
statements:
pub mod module1; // Public modules, so that the contents may be accessible from outside the crate.
pub mod module2;
// ...
You may also keep the modules private and reexport specific items (or modules) with a pub use
statement:
mod module1;
mod module2;
pub use module1::public_function;
pub use module2::Struct1;
It is common for lib.rs
to only contain mod
and pub use
statements, and nothing else.
In Rust, there are no requirements to store a single struct
or enum
and associated code per file.
On the contrary, it is idiomatic for a module to contain multiple functions, struct
or enum
declarations, and impl
blocks with related functionality. For example, you may write all configuration-related items in the config
module.
Code Organization for a Complex Library
As needs grow, you may create nest modules:
- src
- lib.rs
- module1/ # First-level module.
- mod.rs
- submodule1.rs # Nested submodule.
- submodule2.rs # Nested submodule.
- module2.rs
- ...
Cargo.toml
where mod.rs
contains:
pub mod submodule1;
pub mod submodule2;
// ...
Code elsewhere can then refer to public items in the submodules:
// In `module2.rs`:
use super::module1::submodule1;
use super::module1::submodule2::Struct1;
fn a_func() {
submodule1::public_function();
let _struct = Struct1;
}
Nested folders are also possible:
- src
- lib.rs
- module1/
- mod.rs
- submodule1/
- mod.rs
- module2.rs
- ...
Less Common Code Organizations
Less commonly, you may see:
- src
- lib.rs
- module1.rs
- module1/
- submodule1.rs
- submodule2.rs
- module2.rs
- ...
where the module is a file and its submodules are under a folder named after the module.
Finally, module1
can also be defined inline in lib.rs
- especially if the module only contains mod
/ pub use
statements.
// In `lib.rs`:
mod module1 {
pub mod submodule1;
pub mod submodule2;
}
Flatten the Module Hierarchy using Reexports
Instead of working with deeply-nested modules, it is convenient to keep submodules private and reexport items of interest instead.
For example, you may write in module1
:
// Private submodules.
mod submodule1;
mod submodule2;
// Reexport a public function and struct.
pub use submodule1::public_function;
pub use submodule2::Struct1;
This flattens the module hierarchy and hides implementation details. Code in a parent module then refers to:
// The function and struct appear as if they were defined in `module1`.
use module1::public_function;
use module1::Struct1;
See the use
keyword chapter.
Create a Prelude for Commonly Used Items of your Library
Library crates with complex public APIs or deeply nested modules often define a "prelude", a public module that reexports their most commonly used items.
A single glob import e.g., use crate_name::prelude::*;
brings all these common items into scope in one fell swoop and removes the need for repetitive use
declarations:
// src/lib.rs
pub mod prelude {
// Reexport commonly used types, traits, or functions:
pub use super::a_module::SuperCommonType;
pub use super::a_module::super_common_function;
// You may also reexport commonly used items from a dependency of your library:
pub use my_dependency::some_module::CommonType;
}
pub mod a_module {
pub fn super_common_function() { /* ... */ }
pub struct SuperCommonType {
// ...
}
}
// In the client:
use crate_name::prelude::*;
// No need to import common items individually.
The standard library includes a number of preludes. For example, adding use std::io::prelude::*;
at the top of I/O heavy modules imports common I/O traits in one line.
There is also a "Rust prelude", things that Rust automatically imports into every Rust program without even the need for an explicit use
statement. Here is an excerpt:
pub use std::option::Option::{self, Some, None};
pub use std::fmt::{self, Debug, Display};
Code Organization for Binary Crates
Similarly, a simple binary crate may consist of a crate root (e.g. main.rs
) and several modules in separate files and/or folders.
- src
- main.rs
- module1.rs
- module2
- mod.rs
- submodule1.rs
- submodule2.rs
Cargo.toml
Commonly, main.rs
is kept minimal and refers to a library crate in the same src
folder. Indeed, a Rust package can contain both a src/main.rs
binary crate root as well as a src/lib.rs
library crate root, and both crates will have the package name by default.
- src
- lib.rs # Library crate root.
- main.rs # Binary crate root. Imports the library crate.
- module1.rs # Module of the library.
- ...
- tests
- examples
Cargo.toml
In that case, main.rs
imports the public contents of the library crate via use crate_name::module_name;
and only contain a short main()
function.
This code organization exposes a public library crate API, which has the advantages of being reusable and more easily testable (via integration tests in the tests
folder). However, if the crate is published on crates.io
, you must make sure to update your crate version according to Cargo's SemVer (semantic versioning) rules, every time you change the (now public) API. Your code will also break if you decide to rename your crate.
A variation of this code organization puts the command-line argument parsing (or UI) code in modules under main.rs
and keeps the non-user-interface code in the associated library crate.
mod cli; // Command-line argument parsing.
use crate_name::lib_module::*; // Business logic in the library crate.
fn main() {
// ...
}
Organize Large Projects using a Workspace
If your project is large, you may want to split it into several crates, which you then depend on in your main project. For example, you may create a xyz-core
crate, a xyz-derive
crate for procedural macros that let you #[derive(...)]
traits defined in your core crate, a xyz-utils
crate, and a xyz
main crate that binds all subcrates together.
Each crate, of course, should be further split into modules and submodules as needed.
You will most often create a 'Cargo workspace' to tie together your project's crates. A 'workspace' is a set of 'packages' developed in tandem that share the same Cargo.lock
and output (e.g. target
) directory - and therefore share the same dependencies. A package is a bundle of one or more crates with Cargo.toml
file that describes how to build those crates. A package include at least one crate, as many binary crates as you like, but at most only one library crate. Note that the concept of a 'package' is often conflated with that of a 'crate' and the latter word is often used to describe the former. Practically, a package is a subfolder of your workspace that contains a Cargo.toml
file.
A typical organization may look as follows:
# The root folder of your workspace.
- lib1/ # First package subfolder (a library crate).
- src/
- lib.rs
- ...
- tests/ # Optional integration tests and examples.
- examples/
- Cargo.toml
- lib2/ # Second package (a library crate + several optional binary crates).
- src/
- lib.rs
- bin
- tool1.rs # Optional binaries, perhaps tools for e.g. database migration.
- tool2.rs
- ...
- Cargo.toml
- main_lib
- src/
- lib.rs # Often, a main library crate that reexports individual libraries.
- main.rs # Optional binary that uses the library, e.g. a CLI tool.
- ...
- ...
- target/ # Shared output directory.
- Cargo.toml # Workspace Cargo.toml that references the packages.
- Cargo.lock # Shared lock file (and dependencies).
You may use feature flags in the main library crate's Cargo.toml
to selectively build subcrates and their dependencies:
[dependencies]
lib1 = { path = "../lib1" }
lib2 = { path = "../lib2", optional = true }
[features]
feature2 = ["dep:lib2"]
In projects that mix multiple technologies (a web project or a mdbook
that combines markdown and Rust code, like this book), it is common to create a "crates" subdirectory that contains all Rust packages.
The main Cargo.toml
file in the root of the workspace should contain a 'workspace' section that references its packages:
[workspace]
members = [ "lib1", "lib2", "main_lib" ]
Confusingly, a workspace Cargo.toml
can also include a 'root package' in addition to member crates. That lets you place the code of the main library or executable in e.g. a src
folder directly under the workspace root.
Related Topics
- Package Layout.