Question

How to Use a Local Unpublished Crate in Rust

rustrust-crates

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.toml is 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.

Take Quiz

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_lib is a dependency from a local path
  • It builds my_lib
  • It links my_program against 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

  1. You run:
cargo run

from inside my_program.

  1. Cargo reads my_program/Cargo.toml.

  2. It finds this dependency:

my_lib = { path = "../my_lib" }
  1. Cargo goes to the sibling directory ../my_lib and reads its Cargo.toml.

  2. It compiles my_lib as a library crate.

  3. It compiles my_program.

  4. In main.rs, use my_lib::add; makes the add function available in this file.

  5. add(2, 3) returns 5.

  6. 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.

Take Quiz

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

ApproachWhere the crate comes fromWhen to use itExample
Path dependencyLocal filesystemLocal development, internal crates, testing unpublished codemy_lib = { path = "../my_lib" }
crates.io dependencyPublic registryStable public packagesserde = "1.0"
Git dependencyRemote Git repositoryUsing code not yet published, or private hosted codemy_lib = { git = "https://github.com/user/my_lib" }

extern crate vs use

StyleRust eraPurpose
extern crate my_lib;Older Rust editionsExplicitly bring an external crate into scope
use my_lib::thing;Modern RustImport items from a dependency already known to Cargo

Path dependency vs workspace

FeaturePath dependencyWorkspace
Connect one local crate to anotherYesYes
Requires same parent project setupNoUsually yes
Useful for separate sibling foldersYesYes, if organized into one workspace
Best for larger multi-crate developmentSometimesOften

A path dependency solves the immediate problem of using a local unpublished crate. A workspace is more about organizing multiple related crates together.

Take Quiz

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 path is relative to the crate containing that Cargo.toml
  • The library crate should usually contain src/lib.rs
  • Functions, structs, and modules must be pub to be used from another crate
  • In modern Rust, extern crate is 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.toml exist?
  • Does my_lib/src/lib.rs exist?
  • 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.

Take Quiz

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

  1. Create a library crate named text_tools
  2. Add at least one public function in src/lib.rs
  3. Create a binary crate named app
  4. Add text_tools as a path dependency in app/Cargo.toml
  5. Call the library function from app/src/main.rs
Take Quiz

Keep learning

Related questions

View more