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);
}
How this works
- The package name is
my_app - The library crate is also named
my_appby default main.rscan import the library withuse my_app::greet;
Explicit configuration
You can also declare the library and binary explicitly:
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"
[lib]
name = "my_app"
path = "src/lib.rs"
[[bin]]
name = "my_app"
path = "src/main.rs"
[dependencies]
This is usually only needed when:
- you want custom file paths
- you want multiple binaries
- you want names that differ from the defaults
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,use my_app::double;imports the public function from the library crate. - The
mainfunction starts running. numberis set to5.double(number)calls the library function.- The library returns
10. println!prints10.
Important detail
The function in lib.rs must be pub if the binary should use it:
pub fn double(value: i32) -> i32 {
value * 2
}
If you leave off pub, the binary cannot access it.
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
pub mod config;
pub mod parser;
pub mod service;
This lets the library become the main home for application code.
Common pattern: validation and error handling
Developers often place these in the library:
- configuration loading
- input validation
- data transformation
- API logic
- domain rules
Then the binary just coordinates startup and shutdown.
Common pattern: multiple binaries
You can add more binaries under src/bin/:
src/
├── lib.rs
├── main.rs
└── bin/
├── import.rs
└── export.rs
Each binary can use the same library crate.
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:
fn greet() -> String {
"Hello".to_string()
}
Used from main.rs:
use my_app::greet;
This fails because greet is private.
Fixed version:
pub fn greet() -> String {
"Hello".to_string()
}
4. Using the wrong crate name in use
The binary imports the library using the package/library crate name.
If your package is named cool_tool, use:
use cool_tool::greet;
not:
use lib::greet;
5. Putting all logic in main.rs
This works for tiny programs, but becomes hard to test and maintain.
Better:
- keep startup code in
main.rs - move reusable logic to
lib.rs
6. Over-configuring Cargo.toml
Beginners sometimes add [lib] and [[bin]] sections even when they do not need custom names or paths. This is valid, but unnecessary for the default structure.
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
| Approach | Example | When it is best |
|---|---|---|
| Convention-based | src/lib.rs and src/main.rs | Most projects |
Explicit [lib] and [[bin]] | Custom path or name | Non-standard layouts or multiple binaries |
main.rs vs lib.rs
| File | Main purpose | Should contain |
|---|---|---|
src/main.rs | Start the program | argument parsing, setup, top-level error handling |
src/lib.rs | Reusable logic | functions, structs, modules, business logic |
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
- Use
pubfor library items the binary must access - Import the library using the package/crate name
When to add [lib] or [[bin]]
Add them only if you need:
- custom file paths
- custom crate or binary names
- multiple named binaries
Useful command
cargo run
cargo test
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.
What if I want custom file names instead of src/lib.rs and src/main.rs?
Then define [lib] and [[bin]] sections with custom path values in Cargo.toml.
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
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.