Question
I want to create a Rust package that includes both a reusable library, where most of the logic lives, and an executable binary that uses that library.
Assuming my understanding of Rust package and module terminology is correct, what should the Cargo.toml file look like for this setup?
Short Answer
By the end of this page, you will understand how Rust packages can contain both a library crate and one or more binary crates, how Cargo discovers them, what a typical Cargo.toml looks like, and how to structure your files so shared logic lives in src/lib.rs while the executable entry point lives in src/main.rs.
Concept
In Rust, a package is the project managed by Cargo, and a package can contain multiple crates. The most common setup is:
- one library crate for reusable logic
- one binary crate for the executable program
This matters because it lets you separate concerns:
- put core logic in the library so it can be tested and reused
- keep
main.rssmall and focused on startup, argument parsing, and calling library code
A common beginner confusion is mixing up packages, crates, and modules:
- Package: the whole Cargo project, defined by
Cargo.toml - Crate: a compilation unit, such as a library or a binary
- Module: a way to organize code inside a crate
In a Rust package, Cargo uses conventions:
src/lib.rscreates a library cratesrc/main.rscreates a binary crate
If both files exist, the package contains both a library and a binary. In many cases, you do not need special Cargo.toml configuration beyond basic package metadata.
This setup is common in real projects because it improves:
- reusability: other binaries or tests can call the library
- testability: library functions are easier to unit test
- maintainability: business logic stays separate from command-line wiring
Mental Model
Think of your Rust package as a small workshop:
Cargo.tomlis the workshop's label and instructionssrc/lib.rsis the toolbox containing reusable toolssrc/main.rsis the worker who picks up those tools and uses them to do a job
The worker should not build every tool from scratch. Instead, it should call into the toolbox.
So the general idea is:
- library = reusable logic
- binary = entry point that uses the library
That mental model helps you decide where code should go:
- if code is general and reusable, put it in the library
- if code is only for launching the program, put it in
main.rs
Syntax and Examples
The simplest Cargo.toml for a package with both a library and a binary is often just:
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[dependencies]
With this file structure:
my_app/
├── Cargo.toml
└── src/
├── lib.rs
└── main.rs
Example
src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
src/main.rs
use my_app::greet;
fn main() {
let message = greet("Rust");
println!(, message);
}
Step by Step Execution
Consider this example:
src/lib.rs
pub fn double(value: i32) -> i32 {
value * 2
}
src/main.rs
use my_app::double;
fn main() {
let number = 5;
let result = double(number);
println!("{}", result);
}
What happens step by step
- Cargo reads
Cargo.tomland sees a package namedmy_app. - Cargo finds
src/lib.rs, so it builds a library crate namedmy_app. - Cargo finds
src/main.rs, so it also builds a binary crate. - In
main.rs, imports the public function from the library crate.
Real World Use Cases
This package structure is used frequently in Rust projects.
Command-line tools
A CLI app often has:
src/lib.rsfor parsing, business logic, and data processingsrc/main.rsfor reading arguments and printing output
Shared logic across binaries
A project might have multiple executables that all use the same core code.
Examples:
- a
serverbinary - a
workerbinary - an
admintool
All of them can reuse the same library crate.
Testing core functionality
Library code is easier to test than code buried in main.rs.
For example:
- parsing functions
- validation logic
- calculations
- API request helpers
Reusable internal architecture
Even if you never publish the library to crates.io, using a library crate inside the package keeps the code organized and modular.
Real Codebase Usage
In real Rust codebases, developers usually keep main.rs thin and move most logic into the library.
Common pattern: thin main
use my_app::run;
fn main() {
if let Err(err) = run() {
eprintln!("Error: {}", err);
std::process::exit(1);
}
}
src/lib.rs
pub fn run() -> Result<(), String> {
println!("Application is running");
Ok(())
}
This pattern helps with:
- testing
run()directly - centralizing error handling
- avoiding large
main()functions
Common pattern: modules inside the library
src/lib.rs
Common Mistakes
1. Thinking Cargo.toml must always explicitly list the library and binary
For the default layout, this is unnecessary.
Usually enough:
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
If src/lib.rs and src/main.rs exist, Cargo detects both automatically.
2. Confusing modules with crates
A module is not the same as a library crate.
Broken mental model:
modcreates a package entrymodchanges Cargo structure
Correct idea:
modorganizes code inside a cratelib.rsandmain.rsdefine crates in the package
3. Forgetting to mark library items as pub
Broken example:
Comparisons
| Concept | What it means | Typical file | When to use |
|---|---|---|---|
| Package | The whole Cargo project | Cargo.toml | Always |
| Library crate | Reusable compiled code | src/lib.rs | When you want shared logic |
| Binary crate | Executable program entry point | src/main.rs | When you want something runnable |
| Module | Internal code organization inside a crate | mod in .rs files | When splitting code into smaller parts |
Default discovery vs explicit configuration
Cheat Sheet
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
src/
├── lib.rs
└── main.rs
// src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// src/main.rs
use my_app::greet;
fn main() {
println!("{}", greet("world"));
}
Key rules
- A Rust package is defined by
Cargo.toml src/lib.rscreates a library cratesrc/main.rscreates a binary crate- A package can contain both
- Cargo auto-detects both in the default layout
FAQ
Do I need both [lib] and [[bin]] in Cargo.toml?
No. If you use the standard file layout with src/lib.rs and src/main.rs, Cargo finds them automatically.
Can main.rs call functions from lib.rs in the same package?
Yes. Import them using the crate name from Cargo.toml, such as use my_app::some_function;.
What is the difference between a package and a crate in Rust?
A package is the Cargo project. A crate is a compiled target inside that package, such as a library or binary.
Why should I move logic from main.rs into lib.rs?
It makes the code easier to test, reuse, and organize.
Can one package have multiple binaries?
Yes. You can add extra binaries with src/bin/*.rs or explicit [[bin]] entries in Cargo.toml.
Does the library have to be published separately?
No. The library can exist only to organize code inside your package.
Mini Project
Description
Build a small Rust command-line app that keeps its core logic in a library crate and uses a binary crate as the entry point. This demonstrates the standard package structure used in real Rust applications, where reusable logic lives in lib.rs and the executable stays simple.
Goal
Create a Rust package where src/lib.rs provides reusable greeting logic and src/main.rs calls it and prints the result.
Requirements
- Create a Cargo package with both
src/lib.rsandsrc/main.rs - Put the greeting function in the library crate
- Make the function public so the binary can use it
- Import the library function in
main.rs - Run the program and print a greeting message
Keep learning
Related questions
Accessing Cargo Package Metadata in Rust
Learn how to read Cargo package metadata like version, name, and authors in Rust using compile-time environment macros.
Default Function Arguments in Rust: What to Use Instead
Learn how Rust handles default function arguments, why they are not supported, and practical patterns to achieve similar behavior.
Fixing Rust "linker 'cc' not found" on Debian in WSL
Learn why Rust shows "linker 'cc' not found" on Debian in WSL and how to fix it by installing the required C build tools.