Question

Rust Package with Both a Library and a Binary

rustrust-cargo

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.rs small 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.rs creates a library crate
  • src/main.rs creates 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.toml is the workshop's label and instructions
  • src/lib.rs is the toolbox containing reusable tools
  • src/main.rs is 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
Take Quiz

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_app by default
  • main.rs can import the library with use 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

  1. Cargo reads Cargo.toml and sees a package named my_app.
  2. Cargo finds src/lib.rs, so it builds a library crate named my_app.
  3. Cargo finds src/main.rs, so it also builds a binary crate.
  4. In main.rs, use my_app::double; imports the public function from the library crate.
  5. The main function starts running.
  6. number is set to 5.
  7. double(number) calls the library function.
  8. The library returns 10.
  9. println! prints 10.

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.rs for parsing, business logic, and data processing
  • src/main.rs for reading arguments and printing output

Shared logic across binaries

A project might have multiple executables that all use the same core code.

Examples:

  • a server binary
  • a worker binary
  • an admin tool

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.

Take Quiz

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:

  • mod creates a package entry
  • mod changes Cargo structure

Correct idea:

  • mod organizes code inside a crate
  • lib.rs and main.rs define 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

ConceptWhat it meansTypical fileWhen to use
PackageThe whole Cargo projectCargo.tomlAlways
Library crateReusable compiled codesrc/lib.rsWhen you want shared logic
Binary crateExecutable program entry pointsrc/main.rsWhen you want something runnable
ModuleInternal code organization inside a cratemod in .rs filesWhen splitting code into smaller parts

Default discovery vs explicit configuration

ApproachExampleWhen it is best
Convention-basedsrc/lib.rs and src/main.rsMost projects
Explicit [lib] and [[bin]]Custom path or nameNon-standard layouts or multiple binaries

main.rs vs lib.rs

FileMain purposeShould contain
src/main.rsStart the programargument parsing, setup, top-level error handling
src/lib.rsReusable logicfunctions, structs, modules, business logic
Take Quiz

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.rs creates a library crate
  • src/main.rs creates a binary crate
  • A package can contain both
  • Cargo auto-detects both in the default layout
  • Use pub for 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.

Take Quiz

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.rs and src/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
Take Quiz

Keep learning

Related questions

View more