The front door of your code
You finish writing a string parser that handles edge cases beautifully. You want to use it in three different projects. In JavaScript, you drop the file in a utils folder and import it everywhere. In Rust, the compiler stops you. It asks a single question before it will compile anything: what kind of crate is this?
That question determines which file sits at the top of your project tree. Rust calls it the crate root. The root file sets the namespace, controls what gets exposed to the outside world, and tells the compiler whether to build a standalone executable or a reusable library. Get the root wrong, and your code compiles into the wrong shape. Get it right, and the rest of your module tree falls into place.
What a crate root actually does
A crate is a single compilation unit. It is the smallest piece of code that Rust compiles independently. The crate root is the entry point for that unit. When rustc starts, it reads the root file first. That file defines the top-level namespace. Every module, function, and type in your project lives inside the scope that the root establishes.
Think of the root file as the front desk of a building. It decides who gets a key and who stays in the lobby. If you build a standalone application, the front desk hands out a master key and runs the building itself. If you build a library, the front desk packs boxes of components and ships them to other buildings. The physical structure inside might be identical, but the front desk changes how the outside world interacts with it.
Rust enforces this split through two conventional filenames: src/lib.rs and src/main.rs. Cargo uses these names to guess your intent, but the compiler only cares about the crate type flag passed to it. The filename is just a convention that keeps everyone on the same page. The root file also controls the module tree. Rust does not automatically scan your src directory for code. The compiler only compiles files that the root explicitly declares with mod statements. This design prevents accidental namespace collisions and forces you to think about structure before you write logic.
Treat the root file as the architectural blueprint. If it is not wired up there, it does not exist.
The library root: lib.rs
Library crates exist to be imported. They do not run on their own. They export functions, types, and constants that other crates will call. The root file for a library is src/lib.rs.
// src/lib.rs
/// Adds two integers and returns the result.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// Internal helper for formatting error messages.
// Kept private so consumers cannot rely on implementation details.
mod internal {
pub fn format_error(msg: &str) -> String {
format!("[LIB ERROR] {}", msg)
}
}
// Re-export only what the public API needs.
// This keeps the top-level namespace clean and stable.
pub use internal::format_error as public_error;
The compiler reads lib.rs and builds a module tree. It marks every item without pub as private to this crate. It marks pub items as visible to dependencies. When you run cargo build, the compiler produces a .rlib file. That file contains the compiled machine code and metadata. Other crates link against it at compile time.
Notice the visibility boundary. The internal module exists, but its contents are hidden. The pub use statement selectively exposes one function under a new name. This is a standard convention. Library authors rarely expose raw module paths. They flatten the API surface so consumers write my_lib::public_error() instead of my_lib::internal::format_error().
Visibility propagates downward. If you mark a module pub, its children remain private unless you mark them pub as well. This prevents accidental leaks. You can also use pub(crate) to expose items only within the same crate. This is useful for testing helpers that should not reach downstream consumers.
Treat the crate root as a contract. If it is not marked pub, it does not exist for anyone else.
The binary root: main.rs
Binary crates produce executables. They must contain a fn main() entry point. The root file is src/main.rs. Cargo generates this file when you run cargo new my_app.
// src/main.rs
/// Entry point for the command-line tool.
fn main() {
// Import the library function from the dependency.
use my_lib::add;
let sum = add(10, 20);
println!("The sum is {}", sum);
}
The compiler handles main.rs differently. It still builds a module tree, but it looks for the main function at the root level. It wraps that function in startup code that initializes the runtime, sets up standard I/O, and calls your function. The output is an executable file. The binary's namespace is isolated. Nothing inside main.rs is automatically available to other crates. You cannot depend on a binary crate.
This isolation is intentional. Binaries are consumers. Libraries are providers. Mixing the two in a single file creates ambiguity. The compiler needs to know whether to link startup code or pack metadata for downstream consumers. When you compile a binary, the linker resolves all external symbols and produces a standalone binary. When you compile a library, the linker stops at the .rlib stage and leaves symbol resolution to the downstream project.
Keep your main.rs thin. It should parse arguments, call library functions, and print output. Heavy logic belongs in lib.rs.
Mixing libraries and binaries in one project
Real projects often need both. You want to test your logic in isolation, but you also want a command-line interface that uses it. Rust supports a hybrid layout without duplicating code.
You keep src/lib.rs for the shared logic. You add a binary root in src/bin/main.rs. Cargo automatically detects files in src/bin/ and treats each one as a separate binary crate. They all share the same lib.rs as a dependency.
# Cargo.toml
[package]
name = "my_tool"
version = "0.1.0"
# Explicit binary declaration if you skip the src/bin/ directory.
# [[bin]]
# name = "cli"
# path = "src/bin/cli.rs"
// src/bin/cli.rs
/// CLI entry point that depends on the local library.
fn main() {
// The library crate is available under the package name.
let result = my_tool::add(5, 5);
println!("CLI result: {}", result);
}
This pattern separates concerns cleanly. The library gets unit tests. The binary gets integration tests. You can add multiple binaries in src/bin/ without touching lib.rs. The compiler treats each binary as an independent crate that happens to live in the same directory.
Convention dictates that src/bin/ is preferred over [[bin]] sections for simple projects. It keeps Cargo.toml quiet and lets the filesystem declare the structure. When you add a third binary, you just drop a new file in the directory. No manifest edits required.
Visibility traps and compiler rejections
The crate root is where visibility mistakes become fatal. Beginners often mark everything pub to avoid errors. That leaks implementation details and breaks encapsulation. It also triggers warnings when you try to publish.
If you forget pub on a function that another crate needs, the compiler rejects the import with E0603 (function is private). The error points to the root file of the dependency. You must add pub to the item, or add a pub use re-export at the root.
If you accidentally put fn main() inside lib.rs, the compiler ignores it. Library crates do not execute. You will get a warning about an unused function, or the binary will fail to link if you try to run it. The fix is moving main to main.rs or a file in src/bin/.
Another common trap is module organization. You might create src/utils.rs and expect it to be available. It is not. The root file must declare it with mod utils;. Without that declaration, the file is invisible to the compiler. The root controls the module tree. Every branch must be wired up from the top. If you skip the declaration, you get E0432 (unresolved import) when you try to use it.
Watch your re-exports carefully. pub use changes the public API surface. If you re-export a type that changes in a minor version, you break semver for your consumers. Pin your public API at the root. Change it deliberately.
Trust the visibility system. Private items are a feature, not a bug.
Choosing your root file
Use src/lib.rs when you are building a reusable component that other projects will import. Use src/lib.rs when you need unit tests that run independently of a command-line interface. Use src/lib.rs when you want to publish to crates.io or share code across multiple binaries in the same workspace.
Use src/main.rs when you are building a standalone tool that runs from the terminal. Use src/main.rs when the entire project is a single script with no shared logic. Use src/main.rs when you only need a quick prototype and do not plan to import the code elsewhere.
Use a hybrid layout with src/lib.rs and src/bin/ when you need both a testable library and a runnable interface. Use a hybrid layout when you want to ship multiple binaries that share the same core logic. Use a hybrid layout when you want to keep Cargo.toml minimal while maintaining a clean separation between business logic and I/O.
Reach for a workspace instead of a hybrid project when your binaries are completely unrelated. Reach for a workspace when you need separate version numbers for different components.