What Is the Difference Between a Binary and Library Crate?

Binary crates create executable programs with a main function, while library crates provide reusable code for other projects.

The split between running and reusing

You wrote a parser that works perfectly. Now you want to use it in a command-line tool and a background service. You copy the file into two folders. A week later, you fix a bug in the CLI version and forget the service. The service crashes in production. You need a way to write the logic once and use it everywhere. That's the split between binary and library crates.

Rust separates code that runs from code that gets used. A binary crate produces an executable program. A library crate produces code for other programs to import. The distinction isn't just about file names. It shapes how you test, how you share code, and how the compiler links your project.

Binary vs library: the core difference

A binary crate has an entry point. It defines a main function. When you build a binary, the compiler generates an executable file that the operating system can launch. The program starts at main, runs its logic, and exits.

A library crate has no entry point. It defines functions, types, and traits that other code calls. When you build a library, the compiler produces a compiled artifact containing the code, but nothing to run. Other crates link against this artifact to resolve the functions they import.

Think of a binary as a finished script that gets performed. A library is a dictionary of words and grammar rules. The script uses the dictionary, but the dictionary doesn't perform itself.

Minimal examples

Cargo determines the crate type by looking at your file structure. It searches for src/main.rs to build a binary. It searches for src/lib.rs to build a library.

// src/main.rs
/// Entry point for the executable.
/// Cargo looks for this file to build a binary target.
fn main() {
    println!("Running binary");
}
// src/lib.rs
/// Exports a function for other code to use.
/// Cargo looks for this file to build a library target.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

If you have both files, Cargo builds both targets. This is the dual-crate pattern. You get an executable and a reusable library from the same project.

Convention aside: The community often keeps the library name identical to the package name. This lets you write use my_crate::add instead of use my_crate_lib::add. It reduces cognitive load when importing.

What happens when you build

When you run cargo build, Cargo checks the file structure and compiles each target.

If src/main.rs exists, the compiler generates object code with a _start symbol pointing to main. The linker wraps this into an executable file. The executable contains the machine code plus metadata the OS needs to launch it.

If src/lib.rs exists, the compiler generates a .rlib file. This file contains the compiled functions and types but no entry point. Other crates link against this file to resolve symbols. When a binary calls add, the linker patches the call to point to the code inside the .rlib.

Ah-ha moment: The library code compiles once. The binary just points to it. If you change the library, Cargo rebuilds the library and then relinks the binary. This keeps build times fast when you only tweak the CLI wrapper.

Testing works differently for each target. cargo test runs unit tests in both the library and the binary. Integration tests live in the tests/ directory and treat the crate as an external dependency.

Convention aside: Use cargo test --lib to run only library tests. Use cargo test --bin my_app to run only binary tests. This helps isolate failures when you have a large test suite.

Realistic example: a tool with shared logic

Most CLI tools follow the dual-crate pattern. The binary handles arguments and I/O. The library handles the core logic. This split makes the logic testable without spawning a process.

# Cargo.toml
[package]
name = "text_analyzer"
version = "0.1.0"
edition = "2024"

# Explicitly name the library target.
# Omit this section to use the default name matching the package.
[lib]
name = "text_analyzer"
path = "src/lib.rs"
// src/lib.rs
/// Core logic for analyzing text.
/// This module is reusable and testable.

/// Counts words in a string.
pub fn count_words(text: &str) -> usize {
    normalize(text).split_whitespace().count()
}

/// Helper for normalization.
/// Visible to the binary and lib, but not to external users.
pub(crate) fn normalize(text: &str) -> String {
    text.trim().to_lowercase()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn counts_words_correctly() {
        assert_eq!(count_words("  Rust Is Great  "), 3);
    }
}
// src/main.rs
use text_analyzer::count_words;

/// Entry point for the CLI.
/// Delegates to the library for all logic.
fn main() {
    let input = "  Rust Is Great  ";
    println!("Words: {}", count_words(input));
}

The pub(crate) visibility modifier is the secret weapon here. It exposes normalize to the binary and the library, but hides it from external users. This keeps the public API clean while allowing internal sharing.

Convention aside: Mark pub deliberately. Every public item is a promise to external users. If you change a pub signature, you might break downstream code. Use pub(crate) for helpers that only your crate needs.

Pitfalls and compiler errors

If you forget pub on a library function, the compiler rejects any attempt to use it from outside the crate. You get E0603 (private function). The compiler protects you from leaking implementation details. You must mark every public API with pub.

error[E0603]: function `add` is private
 --> src/main.rs:2:20
  |
2 | use my_lib::add;
  |                    ^^^ private function

If your crate only has src/lib.rs and you type cargo run, Cargo complains it has no binary target. You can't run a library. You need a binary to execute.

error: no bin target found.

If you have src/main.rs but no fn main, you get E0601 (main function not defined in a binary crate). The linker needs a start address.

error[E0601]: `main` function not found in crate `my_app`
 --> src/main.rs:1:1
  |
1 | / // No main function here

If you define fn main inside src/lib.rs, the compiler rejects it. Libraries can't have entry points. You get an error about main not being allowed in a library crate.

Convention aside: Keep main thin. The binary should parse arguments, call library functions, and print results. If main contains complex logic, extract it into the library. This makes the logic testable and reusable.

Multiple binaries in one crate

If you need multiple binaries, don't create multiple crates. Use the src/bin/ directory. Cargo treats each file in src/bin/ as a separate binary target. They all link against the same src/lib.rs.

my_project/
├── Cargo.toml
└── src/
    ├── lib.rs
    └── bin/
        ├── cli.rs
        └── server.rs
// src/bin/cli.rs
use my_project::process;

fn main() {
    println!("CLI mode");
    process();
}
// src/bin/server.rs
use my_project::process;

fn main() {
    println!("Server mode");
    process();
}

This structure keeps the project simple. You manage dependencies once. You test the library once. You get multiple executables for free.

Convention aside: Name the binary targets in Cargo.toml if the file names differ from the desired executable names. Otherwise, Cargo uses the file name. This gives you control over the output names.

When to use binary vs library

Use a binary crate when you need an executable program that users run directly. The output is a file you can launch in the terminal or double-click.

Use a library crate when you want to share code across multiple binaries or other libraries. The output is a compiled artifact that links into other programs.

Use a single crate with both src/main.rs and src/lib.rs when your binary is a small wrapper around reusable logic. This keeps the core code testable and shareable without splitting the project.

Reach for a Cargo workspace when you have multiple distinct binaries that share a common library. The workspace lets you manage dependencies once while keeping the binaries separate.

Start with src/lib.rs. Add src/main.rs when you need to run it. This habit forces you to write reusable code from day one.

Where to go next