How to Create a Library Crate in Rust

Run cargo new --lib to create a Rust library crate with a src/lib.rs file for reusable code.

When copy-paste stops working

You spent an afternoon writing a parser that turns messy CSV data into clean structs. It works. You paste it into three different projects. Then you find a bug where commas inside quotes break the logic. Now you have to fix it in three places. Or you write a utility for formatting dates and realize every CLI tool you build needs it. Copy-pasting code feels productive until you have to update it. That's when you need a library crate.

Package the code once. Import it everywhere.

What a library crate is

A library crate is a package of code meant to be used by other code, not run directly. Think of it as a shared toolkit. You build the tools once. Other projects import them and use them. In Rust, a "crate" is the unit of compilation. A binary crate produces an executable. A library crate produces a compiled artifact that other crates can link against.

The file src/lib.rs is the entry point for a library. It defines what the world gets to see. When you depend on a library, the compiler reads lib.rs to understand the public API. Everything else is implementation detail.

Libraries are imported, not executed. They run inside the process of the code that uses them.

Creating the crate

Run cargo new --lib to generate the skeleton. The --lib flag tells Cargo to create a library instead of a binary. This changes the root file from src/main.rs to src/lib.rs.

cargo new --lib my_utils

Cargo creates the directory structure and a Cargo.toml configured for a library. The Cargo.toml includes name, version, and edition. The edition field determines which version of the Rust language rules apply. Always set this explicitly.

[package]
name = "my_utils"
version = "0.1.0"
edition = "2021"

The convention is to use the latest stable edition. Rust editions allow the language to evolve without breaking old code. Setting edition = "2021" ensures you get modern features like improved macro hygiene and better error messages.

Run the command. Check that src/lib.rs exists.

The entry point and visibility

Code in src/lib.rs is private by default. If you define a function without pub, other crates cannot see it. The compiler enforces this boundary. If you try to use a private function from outside the crate, you get E0603 (item is private).

// src/lib.rs

/// Adds two integers.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Internal helper. Not exposed.
fn internal_helper() {
    // This function is only visible inside this crate.
}

The pub keyword marks an item as part of the public API. Users of your crate can call my_utils::add. They cannot call internal_helper. This encapsulation lets you change internal logic without breaking users.

Add pub to share. Leave it off to hide.

Structuring with modules

Real libraries grow. You can't put everything in lib.rs. Use modules to organize code. A module groups related items. You declare a module with mod and define it in a file.

// src/lib.rs

/// Math utilities.
pub mod math {
    /// Adds two integers.
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }
}

/// Text processing helpers.
pub mod text {
    /// Reverses a string slice.
    pub fn reverse(s: &str) -> String {
        s.chars().rev().collect()
    }
}

When you write pub mod math, the compiler looks for the definition. The modern convention is to put the module in a file named math.rs inside src. Do not use math/mod.rs. That pattern is legacy. The compiler supports both, but the community prefers foo.rs for single-file modules. It keeps the directory tree flat and readable.

src/
├── lib.rs
├── math.rs
└── text.rs

Organize by domain, not by file size.

Curating the public API

Modules create a hierarchy. Users have to type my_utils::math::add to use the function. That's fine for small tools. For larger libraries, deep paths get tedious. Use pub use to re-export items and flatten the API.

// src/lib.rs

pub mod math;
pub mod text;

// Re-export common items at the root.
pub use math::add;
pub use text::reverse;

Now users can write my_utils::add or my_utils::reverse. The module structure still exists for organization, but the public interface is cleaner. This is the "facade" pattern. You hide the internal folder structure and present a curated set of tools.

Good libraries hide complexity. Re-export types and functions so users don't see your implementation details.

Testing the library

Libraries need tests. Rust makes this easy with #[cfg(test)]. This attribute tells the compiler to include the code only when running tests. The test code doesn't bloat the compiled library.

// src/lib.rs

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    // Import everything from the parent module, including private items.
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    fn test_add_negative() {
        assert_eq!(add(-1, 1), 0);
    }
}

The use super::* line is key. It brings all items from the parent module into scope, including private ones. This lets tests verify internal behavior without exposing it publicly. Tests are the only code that can see your secrets.

Run cargo test to execute the tests. Cargo compiles the library with test configuration, runs the test harness, and reports results.

Tests are part of the library. Write them before you publish.

Metadata and configuration

The Cargo.toml file controls how your library is built and published. Beyond name and version, you can add metadata for crates.io.

[package]
name = "my_utils"
version = "0.1.0"
edition = "2021"
description = "A collection of math and text utilities."
repository = "https://github.com/username/my_utils"
license = "MIT"

The description appears in search results. The repository links to your source code. The license tells users how they can use your code. These fields help others discover and trust your crate.

Set the edition. Rust moves fast. Pin your version to avoid surprises.

Pitfalls

If you add fn main to a library crate, the compiler rejects it. Libraries don't have entry points. The error message points out that main is not allowed in a library. If you need a binary to test your library or provide a CLI tool, add a src/bin/ directory. Files in src/bin/ are compiled as separate executables that can depend on the library.

src/
├── lib.rs
└── bin/
    └── cli_tool.rs

This structure lets you share logic between the library and the tool. The tool imports the library. Users can depend on the library without pulling in the binary.

Libraries don't have main. Use src/bin/ for executables.

Visibility rules

Visibility is granular. pub exposes an item to the world. pub(crate) exposes it only within the crate. pub(super) exposes it to the parent module. Use these modifiers to control access precisely.

// src/lib.rs

/// Visible everywhere.
pub fn public_api() {}

/// Visible only inside this crate.
pub(crate) fn internal_util() {}

// src/math.rs

/// Visible only in the parent module (lib.rs).
pub(super) fn math_helper() {}

This helps you build internal abstractions without leaking them. You can refactor internal code freely as long as the public API stays stable.

The compiler enforces your visibility. Trust it to keep your internals hidden.

Decision matrix

Use cargo new --lib when you are building code that other projects will import. Use cargo new when you are building a standalone executable that runs directly. Use a workspace when you have multiple crates that depend on each other and you want to build them together. Reach for pub(crate) when you need internal helpers that shouldn't leak into the public API. Reach for pub use when you want to flatten the API and hide module structure. Reach for #[cfg(test)] when you need test code that doesn't ship with the library.

Pick the crate type that matches the output you need.

Where to go next