Question
I created a Rust library crate with:
cargo new my_lib
and I want to use that library from a separate Rust binary project:
cargo new my_program --bin
In my program, I tried to write:
extern crate my_lib;
fn main() {
println!("Hello, World!");
}
What do I need to do to make this work?
The two crates are not inside the same project folder structure beyond being siblings:
.
├── my_lib
└── my_program
I expected I could override the path based on the Cargo guide, but I found a note saying:
You cannot use this feature to tell Cargo how to find local unpublished crates.
This is with a stable Rust toolchain.
Short Answer
By the end of this page, you will understand how Rust and Cargo connect one crate to another using dependencies in Cargo.toml. You will learn how to use a local unpublished crate with a path dependency, how Cargo resolves it, what folder structure is allowed, and how this differs from crates published on crates.io.
Concept
In Rust, one crate does not automatically know about another crate just because both folders exist on your machine. Cargo only includes crates that are explicitly declared as dependencies.
For a local unpublished crate, the usual solution is a path dependency. That means your application tells Cargo, “this dependency lives in this folder on disk.”
For example, if my_program depends on my_lib, you declare that relationship in my_program/Cargo.toml.
[dependencies]
my_lib = { path = "../my_lib" }
This matters because Cargo is a build system and package manager. It needs a reliable, explicit way to know:
- which crates your project depends on
- where to find them
- when to rebuild them
- which versions or sources to use
A local crate can be used even if it is unpublished, as long as:
- it is a valid Cargo package
- it has a
Cargo.toml - the path points to the correct folder
In modern Rust, once the dependency is declared, you usually do not need extern crate in most cases. You can import items directly with use, or refer to them by crate name.
The important idea is this:
- Rust code uses crate names
- Cargo decides where those crates come from
Cargo.tomlis where that mapping is configured
Mental Model
Think of Cargo as a delivery manager for your project.
Your Rust code says, “I need my_lib.” But Cargo asks, “Where should I get it from?”
There are several possible answers:
- from crates.io
- from a Git repository
- from a local folder on disk
A path dependency is like giving Cargo a street address:
my_lib = { path = "../my_lib" }
Now Cargo knows exactly where to pick up that crate.
Without that address, writing use my_lib::... or extern crate my_lib; is like asking for a package without telling the delivery manager where it lives.
Syntax and Examples
Basic syntax
In the dependent crate's Cargo.toml:
[dependencies]
my_lib = { path = "../my_lib" }
This says:
- dependency name:
my_lib - source: the local folder
../my_lib
Example project structure
.
├── my_lib
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
└── my_program
├── Cargo.toml
└── src
└── main.rs
Library crate
my_lib/src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
Program crate
my_program/Cargo.toml
[package]
name = "my_program"
version = "0.1.0"
edition = "2021"
[dependencies]
my_lib = { path = "../my_lib" }
my_program/src/main.rs
use my_lib::greet;
fn main() {
println!("{}", greet("World"));
}
What this does
- Cargo reads
my_program/Cargo.toml - It sees
my_libis a dependency from a local path - It builds
my_lib - It links
my_programagainst that library - Your code can then call
my_lib::greet
Older Rust code
In older Rust editions, you may see:
extern crate my_lib;
fn main() {
println!("Hello");
}
That was more common before Rust 2018. Today, in most cases, you can just write:
use my_lib::greet;
Step by Step Execution
Consider this code:
my_lib/src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
my_program/Cargo.toml
[dependencies]
my_lib = { path = "../my_lib" }
my_program/src/main.rs
use my_lib::add;
fn main() {
let result = add(2, 3);
println!("{}", result);
}
Step by step
- You run:
cargo run
from inside my_program.
-
Cargo reads
my_program/Cargo.toml. -
It finds this dependency:
my_lib = { path = "../my_lib" }
-
Cargo goes to the sibling directory
../my_liband reads itsCargo.toml. -
It compiles
my_libas a library crate. -
It compiles
my_program. -
In
main.rs,use my_lib::add;makes theaddfunction available in this file. -
add(2, 3)returns5. -
println!prints:
5
The key point is that Rust source code alone does not locate the crate. Cargo does that first using the dependency declaration.
Real World Use Cases
Local unpublished crates are very common in real Rust development.
Shared internal libraries
A team may create a reusable internal crate for:
- validation helpers
- logging utilities
- configuration loading
- domain models
That crate may never be published publicly.
Multi-project development
You might build:
- one crate for business logic
- one binary crate for a CLI
- one binary crate for a server
Both binaries can depend on the same local library.
Prototyping before publishing
Many developers start with a local crate first. Once the API stabilizes, they may publish it later.
Monorepo-style setups
Even if crates are in separate folders, local path dependencies let you develop them together without publishing every change.
Testing a library in a real app
A path dependency is useful when you want to try a new library feature immediately in another project before releasing a version.
Real Codebase Usage
In real codebases, developers usually combine local crates with a few common patterns.
Reusable modules extracted into crates
When code becomes shared across multiple binaries or services, teams often move it into a local crate instead of copying files between projects.
Clear public API design
A library crate usually exposes only what consumers need:
pub fn parse_config() {}
and keeps internal helpers private.
Validation and domain logic
A local crate might centralize rules so multiple apps behave the same way.
For example:
- input validation
- pricing calculations
- serialization helpers
- authentication utilities
Incremental development
During development, path dependencies are convenient because changes in the library are picked up when you rebuild the dependent crate.
Workspaces as a next step
In larger projects, developers often move related crates into a Cargo workspace. A workspace is not required for path dependencies, but it helps when:
- building multiple crates together
- sharing
Cargo.lock - running tests across several crates
Common pattern: binary + library
A binary crate often stays small and delegates most logic to a library crate:
use my_lib::run;
fn main() {
run();
}
This makes testing easier and keeps main.rs simple.
Common Mistakes
1. Forgetting to add the dependency to Cargo.toml
Writing this in Rust code is not enough:
use my_lib::greet;
If Cargo.toml does not include my_lib, Cargo cannot resolve it.
Fix
[dependencies]
my_lib = { path = "../my_lib" }
2. Using the wrong path
Broken example:
[dependencies]
my_lib = { path = "./my_lib" }
If my_program and my_lib are sibling folders, ./my_lib points to the wrong place.
Fix
[dependencies]
my_lib = { path = "../my_lib" }
3. Expecting folder names alone to make crates visible
Just because this exists:
.
├── my_lib
└── my_program
does not mean my_program can use my_lib automatically.
Fix
Declare the dependency explicitly in my_program/Cargo.toml.
4. Mixing old and modern import styles
Older code may use:
extern crate my_lib;
Modern Rust usually prefers:
use my_lib::some_function;
Both are about using the crate in code, but the dependency still must be declared in Cargo.toml.
5. Library crate does not expose anything public
Broken library:
fn greet() {
println!("Hello");
}
This function is private.
Fix
pub fn greet() {
println!("Hello");
}
6. Creating the wrong crate type
If you want a reusable library, the crate should have src/lib.rs. A binary-only crate with only src/main.rs cannot be used as a normal library dependency.
Fix
Create a library crate or add a library target.
Comparisons
| Approach | Where the crate comes from | When to use it | Example |
|---|---|---|---|
| Path dependency | Local filesystem | Local development, internal crates, testing unpublished code | my_lib = { path = "../my_lib" } |
| crates.io dependency | Public registry | Stable public packages | serde = "1.0" |
| Git dependency | Remote Git repository | Using code not yet published, or private hosted code | my_lib = { git = "https://github.com/user/my_lib" } |
extern crate vs use
| Style | Rust era | Purpose |
|---|---|---|
extern crate my_lib; | Older Rust editions | Explicitly bring an external crate into scope |
use my_lib::thing; | Modern Rust | Import items from a dependency already known to Cargo |
Path dependency vs workspace
| Feature | Path dependency | Workspace |
|---|---|---|
| Connect one local crate to another | Yes | Yes |
| Requires same parent project setup | No | Usually yes |
| Useful for separate sibling folders | Yes | Yes, if organized into one workspace |
| Best for larger multi-crate development | Sometimes | Often |
A path dependency solves the immediate problem of using a local unpublished crate. A workspace is more about organizing multiple related crates together.
Cheat Sheet
# my_program/Cargo.toml
[dependencies]
my_lib = { path = "../my_lib" }
// my_program/src/main.rs
use my_lib::some_function;
fn main() {
some_function();
}
Rules to remember
- Local crates must be declared in
Cargo.toml - The
pathis relative to the crate containing thatCargo.toml - The library crate should usually contain
src/lib.rs - Functions, structs, and modules must be
pubto be used from another crate - In modern Rust,
extern crateis usually unnecessary
Common path example
If the structure is:
.
├── my_lib
└── my_program
then inside my_program/Cargo.toml use:
my_lib = { path = "../my_lib" }
Quick checks
- Does
my_lib/Cargo.tomlexist? - Does
my_lib/src/lib.rsexist? - Is the dependency declared under
[dependencies]? - Is the path correct?
- Are the items you want to use marked
pub?
FAQ
Can I use a local crate without publishing it to crates.io?
Yes. Add it as a path dependency in Cargo.toml.
Do the two crates need to be in the same folder?
No. They can be anywhere, as long as the path points to the correct location.
Why does extern crate my_lib; not work by itself?
Because Rust code does not tell Cargo where to find the crate. You must declare the dependency in Cargo.toml.
What path should I use for sibling folders?
If my_program and my_lib are siblings, use:
my_lib = { path = "../my_lib" }
Does a local crate need lib.rs?
If you want to use it as a library dependency, yes, it should provide a library target, usually through src/lib.rs.
Should I use extern crate in modern Rust?
Usually no. In Rust 2018 and later, use statements are typically enough once the dependency is declared.
When should I use a workspace instead of just a path dependency?
Use a workspace when multiple related crates are developed together and you want shared tooling, builds, and dependency management.
Mini Project
Description
Create a small Rust setup where one local library crate provides reusable text utilities and a separate binary crate uses it through a path dependency. This demonstrates the exact workflow for local unpublished crates in a practical way.
Goal
Build a binary program that imports and uses functions from a local library crate stored in a different folder.
Requirements
- Create a library crate named
text_tools - Add at least one public function in
src/lib.rs - Create a binary crate named
app - Add
text_toolsas a path dependency inapp/Cargo.toml - Call the library function from
app/src/main.rs
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.