Rust Development Environment

main.rs and lib.rs

In a Cargo package, src/main.rs and src/lib.rs are the two primary crate root files. src/main.rs is the entry point for compiling an executable binary crate, while src/lib.rs is the entry point for compiling a reusable library crate.

Roles of src/main.rs vs src/lib.rs

Property src/main.rs (Binary Crate Root) src/lib.rs (Library Crate Root)
Crate type (--crate-type) bin (executable program) lib / rlib (reusable library)
Requires fn main()? Yes (E0601 if missing) No (main has no special meaning in lib.rs)
Can other packages depend on it? No (external crates cannot import items from a binary crate) Yes (pub items in lib.rs form the package's public API)
Can integration tests (tests/*.rs) import it? No Yes (integration tests import the library crate by name)
Maximum count per Cargo package 1 default (src/main.rs) + any number in src/bin/*.rs At most 1 (src/lib.rs) per package

Using Both src/lib.rs and src/main.rs in the Same Package

A single Cargo package can contain both src/lib.rs and src/main.rs simultaneously! When both files are present, Cargo builds two separate crates that share the package's name:

  1. First, Cargo compiles src/lib.rs as a library crate.
  2. Next, Cargo compiles src/main.rs as a binary crate that automatically has the library crate available as a dependency under the package's name.
my_scanner/ (Cargo Package)
├── Cargo.toml
└── src/
    ├── lib.rs      ---> Compiles to library crate `my_scanner` (core logic & public types)
    └── main.rs     ---> Compiles to binary executable `my_scanner` (CLI parsing + calls `my_scanner::...`)

Why the Dual lib.rs + main.rs Pattern Is Idiomatic

Even when building a standalone command-line application, experienced Rust developers place the core data structures and business logic in src/lib.rs and keep src/main.rs as a thin wrapper (handling only CLI argument parsing, configuration loading, and exit codes):

// src/lib.rs — Library crate (`word_counter`)
pub struct Stats {
    pub lines: usize,
    pub words: usize,
}

pub fn analyze(text: &str) -> Stats {
    Stats {
        lines: text.lines().count(),
        words: text.split_whitespace().count(),
    }
}
// src/main.rs — Binary crate (`word_counter`)
use word_counter::analyze; // Imports the sibling library crate `src/lib.rs`!

fn main() {
    let sample = "Rust separates library logic\nfrom binary entry points.";
    let stats = analyze(sample);

    println!("Lines: {}, Words: {}", stats.lines, stats.words);
}
Lines: 2, Words: 8

Structuring an application this way provides three immediate benefits:

  1. Integration testing (tests/): Files in the tests/ directory can only import a package's library crate (src/lib.rs), never src/main.rs. Moving logic into src/lib.rs makes the entire application testable via integration tests and doctests.
  2. Code reuse across multiple binaries: If you add helper binaries in src/bin/, every binary can import shared functions from src/lib.rs without duplicating code.
  3. Fast compilation checks: Library API boundaries stay clean and separated from CLI I/O concerns.

Avoid Declaring the Same Module in Both lib.rs and main.rs

A common beginner mistake in a package with both src/lib.rs and src/main.rs is writing mod helper; in both src/lib.rs and src/main.rs:

❌ Anti-Pattern (Compiles `helper.rs` TWICE as two distinct types!):
  src/lib.rs  ---> `pub mod helper;`
  src/main.rs ---> `mod helper;`     (Creates a duplicate `main::helper` module!)

✅ Correct Pattern (Declare `pub mod helper;` in `lib.rs`, and `use` it in `main.rs`):
  src/lib.rs  ---> `pub mod helper;`
  src/main.rs ---> `use my_pkg::helper;`

If mod helper; appears in both src/lib.rs and src/main.rs, rustc compiles src/helper.rs twice—once inside the library crate and once inside the binary crate—causing dead-code warnings and distinct incompatible types (my_pkg::helper::Config vs crate::helper::Config). Always declare shared modules in src/lib.rs with pub mod and import them into src/main.rs with use <pkg_name>::<module>;.