Modules, crates and project structure
mod, pub and use, library versus binary targets, file and folder layout, workspaces, feature flags, dependency semver, and generated docs.
Modules, visibility and paths
// src/lib.rs — the crate root of a library
pub mod store; // loads src/store.rs or src/store/mod.rs
pub mod config;
mod internal { // inline module
pub(crate) fn helper() {} // visible anywhere in this crate
fn secret() {} // visible only inside internal
}
pub use crate::store::Store; // re-export: the public path becomes crate::Store
pub use internal::helper as helper_fn;
pub fn run() {
let s = Store::open(":memory:");
internal::helper();
s.close();
}
// use brings names into scope; paths are absolute from crate:: or the crate root
use std::collections::HashMap;
use std::fmt::{self, Display};- Everything is private by default, and the default is the correct one. Add
pubwhen a caller outside needs it, andpub(crate)when only your own code does. - A private item is visible to its own module and to descendants, which is why a private helper inside
internalis reachable from nested modules below it. pub useflattens a deep internal structure into a small public surface. Users importcrate::Store, and you stay free to move the file that defines it.self::andsuper::are the relative paths:superis the parent module. Prefer the absolutecrate::path — it survives moving the file.- In edition 2018 and later,
use some_crate::Thingrefers to a dependency without::prefixes. There is no need forextern crate.
Layout: lib, bin and folders
notes/
Cargo.toml
src/
lib.rs library crate root: the reusable logic
main.rs binary crate root: a thin main that calls the library
store.rs module at src/store.rs
config/
mod.rs module in a folder: submodules declared here
parse.rs
env.rs
bin/
migrate.rs an extra binary, built as notes-migrate
tests/
integration.rs integration tests: only the public API is visible
benches/
encode.rs
examples/
quickstart.rs
doc/| Path | What cargo does with it |
|---|---|
src/lib.rs | Builds a library target named after the package; other crates can depend on it |
src/main.rs | Builds a binary named after the package |
src/bin/*.rs | One extra binary per file, named after the file |
tests/*.rs | Integration tests, each compiled as its own crate against your public API |
examples/*.rs | Compiled on demand with cargo run --example name; also checked by cargo test |
benches/*.rs | Benchmarks, run with cargo bench |
// src/main.rs — the binary is a thin shell around the library
use notes::{config, Store};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cfg = config::load("notes.toml")?;
let mut store = Store::open(&cfg.path);
store.run()?;
Ok(())
}
// a module can be declared in a folder with mod.rs, or as store.rs + store/ for
// submodules. Pick one convention per project: mixed styles are the usual cause of
// "file not found for module" errors.
// feature flags let one crate serve several profiles
#[cfg(feature = "sqlite")]
pub mod sqlite;
#[cfg(not(feature = "sqlite"))]
pub mod memory;[features]
default = ["memory"]
memory = []
sqlite = ["dep:rusqlite"]
full = ["sqlite", "metrics"]
[dependencies]
rusqlite = { version = "0.32", optional = true }Dependencies, semver and docs
- Features must be additive. Enabling a feature may add code but must never remove or change existing behaviour, because Cargo unifies features across all dependents of a crate.
- Never make a feature that silently switches an implementation for everyone. If two behaviours must be chosen, make them two crates or two types.
dep:rusqlitein a feature list hides the optional dependency from the feature namespace; the older implicit feature of the same name is the reason this syntax exists.- Semver in Cargo:
0.xversions are incompatible with each other forx > 0, so0.9to0.10is a breaking change even though the major digit is still zero. Pin the minor for pre-1.0 dependencies. cargo tree -dlists crates present in more than one version. Duplicates are legal and often unavoidable, but they cost compile time and binary size.cargo doc --no-deps --openbuilds the API documentation, andcargo doc --document-private-itemsincludes internals while you design.
/// Opens a store at the given path.
///
/// # Errors
///
/// Returns an error if the path is not writable.
///
/// # Examples
///
/// ```
/// let store = notes::Store::open(":memory:");
/// assert!(store.is_open());
/// ```
pub fn open(path: &str) -> Store { /* ... */ }
// #![warn(missing_docs)] at the top of lib.rs turns missing docs into a warning💡
Write the library first and the binary second. Keeping logic in
src/lib.rs means integration tests, examples and future binaries all reach it through the public API, and the compiler checks that API from the outside. A program with everything in main.rs can only be tested from the command line.FAQ
mod.rs or the newer file style?
Both work.
store.rs plus a store/ folder is the modern style and avoids a screen full of mod.rs tabs. The only rule is to be consistent, because mixing the two in one crate produces confusing module-resolution errors.Why can my integration test not see an item?
Tests under
tests/ are separate crates and see only what is pub. Either export the item, or move the test into a #[cfg(test)] module inside the source file, where private items are reachable.Related
Setting up Rust: rustup, Cargo and edition 2024 Testing, documentation and CI
Last refreshed 2026-09-18.