Question

Understanding Rust Modules with main.rs and lib.rs

rustmodule

Question

I have these four Rust source files:

// src/main.rs
mod bar;

fn main() {
    let v = vec![1, 2, 3];
    println!("Hello, world!");
}
// src/lib.rs
pub mod foo;
pub mod bar;
// src/foo.rs
pub fn say_foo() {

}
// src/bar.rs
use crate::foo;

fn bar() {
    foo::say_foo();
}

When I run cargo run, I get this error:

error[E0432]: unresolved import `crate::foo`
 --> src/bar.rs:1:5
  |
1 | use crate::foo;
  |     ^^^^^^^^^^ no `foo` in the root

How should this be fixed? More broadly, how does Rust module lookup work when a project has both main.rs and lib.rs?

I also noticed that adding mod foo; to main.rs fixes the issue, but I do not understand why. I thought lib.rs was the place where modules were exposed. Why does main.rs need its own module declaration too?

For reference, the Cargo.toml is:

[package]
name = "hello-world"
version = "0.1.0"
authors = ["me@mgail.com>"]
edition = "2018"

[dependencies]

Short Answer

By the end of this page, you will understand that src/main.rs and src/lib.rs define two different crate roots in Rust. You will see why crate::foo fails inside code compiled as part of the binary crate, how module declarations are local to each crate, and how to structure shared code so both your binary and library work correctly.

Concept

In Rust, a package can contain multiple crates. The most common case is:

  • src/main.rs → a binary crate
  • src/lib.rs → a library crate

Even though both files live in the same package, they are not the same crate.

That is the key idea.

What crate means

Inside Rust code, crate:: always refers to the root of the current crate.

So:

  • in main.rs, crate means the binary crate rooted at main.rs
  • in lib.rs, crate means the library crate rooted at lib.rs

These roots are separate.

Why your code fails

Your main.rs contains:

mod bar;

That means the binary crate includes a module named bar from src/bar.rs.

But main.rs does not declare:

mod foo;

So inside the binary crate, there is no crate::foo.

Now look at src/bar.rs:

use crate::foo;

When bar.rs is compiled as part of the binary crate, Rust tries to find foo in the root of that binary crate. It checks what main.rs declared. Since main.rs only declared mod bar;, there is no foo, so you get the error.

What lib.rs does and does not do

lib.rs exposes modules for the library crate only.

This code:

pub mod foo;
pub mod bar;

creates modules in the library crate's module tree. It does not automatically inject those modules into main.rs.

So lib.rs is not a global module registry for the whole package. It is only the root of the library crate.

Why this matters

Understanding this separation helps you:

  • avoid confusing module errors
  • organize reusable code in lib.rs
  • keep main.rs small and focused on starting the program
  • write binaries that use the library crate cleanly

A very common real-world pattern is:

  • put shared logic in lib.rs
  • import that library from main.rs
  • keep main.rs mostly for command-line parsing, setup, and calling library functions

Mental Model

Think of main.rs and lib.rs as two separate buildings, each with its own front door and room map.

  • main.rs is one building
  • lib.rs is another building

A module declaration like mod bar; adds a room to the building you are currently in.

So if you write mod bar; in main.rs, that creates bar inside the main building. If you write pub mod foo; in lib.rs, that creates foo inside the library building.

Now imagine code in bar.rs says:

Go to the room foo in this building.

That is what crate::foo means.

If bar.rs is being used by the binary crate, then crate means the main building. If the main building does not have a foo room, the lookup fails.

So the important mental rule is:

  • crate::... means look inside the current crate's module tree
  • main.rs and lib.rs build different trees
Take Quiz

Syntax and Examples

Core syntax

Declaring modules in a crate root

// src/lib.rs
pub mod foo;
pub mod bar;

This tells Rust that the library crate contains modules from src/foo.rs and src/bar.rs.

// src/main.rs
mod bar;

This tells Rust that the binary crate contains a module from src/bar.rs.

These declarations are independent.

Example 1: Why your current setup fails

// src/main.rs
mod bar;

fn main() {
    println!("Hello");
}
// src/bar.rs
use crate::foo;

This fails because the current crate is the binary crate, and its root only declares bar, not foo.

Example 2: Fix by declaring both modules in the binary crate

// src/main.rs
mod foo;
mod bar;

fn main() {
    println!("Hello");
}
// src/foo.rs
pub fn say_foo() {
    println!("foo");
}
// src/bar.rs
use crate::foo;

pub fn bar() {
    foo::say_foo();
}

This works because now the binary crate has both foo and bar.

Example 3: Preferred pattern—put shared code in the library crate

A cleaner setup is to define your reusable modules in lib.rs and then use the library from main.rs.

// src/lib.rs
pub mod foo;
pub mod bar;
// src/foo.rs
pub fn say_foo() {
    println!("foo");
}
// src/bar.rs
use crate::foo;

pub fn bar() {
    foo::say_foo();
}
// src/main.rs
fn main() {
    hello_world::bar::bar();
}

Here, hello_world is the library crate name taken from Cargo.toml.

This works because:

  • foo and bar are both part of the library crate
  • inside bar.rs, crate::foo refers to the library crate root
  • main.rs uses the library crate as a consumer, just like any other crate would

Important note

If a function should be called from outside its module, it must be pub:

pub fn bar() {
    // ...
}

Step by Step Execution

Consider this working version:

// src/lib.rs
pub mod foo;
pub mod bar;
// src/foo.rs
pub fn say_foo() {
    println!("foo");
}
// src/bar.rs
use crate::foo;

pub fn bar() {
    foo::say_foo();
}
// src/main.rs
fn main() {
    hello_world::bar::bar();
}

Step-by-step

1. Cargo builds the library crate

Rust starts from src/lib.rs.

It sees:

pub mod foo;
pub mod bar;

So it includes:

  • src/foo.rs as crate::foo
  • src/bar.rs as crate::bar

At this moment, inside bar.rs, crate refers to the library crate.

2. bar.rs imports crate::foo

use crate::foo;

This succeeds because lib.rs declared foo.

3. Cargo builds the binary crate

Rust also builds src/main.rs as a separate crate.

fn main() {
    hello_world::bar::bar();
}

The binary crate does not need mod foo; or mod bar; because it is not defining those modules itself. Instead, it is calling into the library crate named hello_world.

4. Program runs

main() calls:

hello_world::bar::bar();

That function calls:

foo::say_foo();

And the output is:

foo

Why your original version breaks

Original main.rs:

mod bar;

This tells Rust to compile bar.rs as part of the binary crate.

Then in bar.rs:

use crate::foo;

Rust looks for foo in the binary crate root. But binary crate root is main.rs, and it never declared mod foo;. So lookup fails.

Real World Use Cases

1. Command-line applications

A common Rust CLI structure is:

  • lib.rs contains parsing logic, validation, helpers, and business rules
  • main.rs handles argument reading and starts execution

This keeps the app easier to test.

2. Web servers and APIs

In a web app, you might place these in the library crate:

  • route handlers
  • models
  • validation
  • database logic

Then main.rs just bootstraps the server.

3. Shared logic across multiple binaries

A package can have several binaries in src/bin/ that all use the same lib.rs.

That is only practical if you understand that the library crate is separate and reusable.

4. Testing

Library code is easier to test because it is designed to be imported.

For example:

// src/lib.rs
pub mod math;
// tests/math_test.rs
use hello_world::math;

#[test]
fn adds_numbers() {
    assert_eq!(math::add(2, 3), 5);
}

5. Reusable packages

If you eventually publish a crate, lib.rs defines the API other projects will import. main.rs is only for running your local executable.

Take Quiz

Real Codebase Usage

In real Rust projects, developers usually avoid duplicating module declarations across main.rs and lib.rs unless they truly want separate implementations.

Common pattern: thin main.rs

A very common layout is:

// src/main.rs
fn main() {
    my_crate::run();
}
// src/lib.rs
pub mod config;
pub mod app;

pub fn run() {
    // orchestrate program logic
}

This has several benefits:

  • shared code lives in one place
  • tests can target library functions
  • binaries stay simple
  • module paths are easier to reason about

Visibility patterns

Developers often use:

  • pub mod x; when exposing a module from the library root
  • mod x; when the module is internal only
  • pub fn for functions that must be accessible from other modules or crates

Internal module references

Inside a library crate, code often uses:

use crate::config::Settings;

This is clean and stable because crate refers to one known root.

Guarding complexity

Real codebases often avoid putting too much logic in main.rs because:

  • it is harder to reuse
  • it is harder to test directly
  • module layout becomes confusing if both main.rs and lib.rs define overlapping modules

Practical advice

If code is shared or reusable, put it under lib.rs. If code is only for starting the program, keep it in main.rs. If you find yourself declaring the same modules in both places, pause and ask whether the code should just live in the library crate instead.

Common Mistakes

1. Assuming lib.rs and main.rs share one module tree

They do not.

Broken assumption:

// main.rs
mod bar;

// bar.rs
use crate::foo;
// lib.rs
pub mod foo;

Why it breaks:

  • bar.rs is compiled under the binary crate
  • foo only exists in the library crate

How to avoid it:

  • either declare foo in main.rs too
  • or, better, move shared modules to lib.rs and use them from main.rs

2. Forgetting pub

Broken code:

// bar.rs
fn bar() {
    // ...
}
// main.rs
fn main() {
    hello_world::bar::bar();
}

This fails because bar() is private.

Fix:

pub fn bar() {
    // ...
}

3. Reusing the same file in confusing ways

If both main.rs and lib.rs declare the same module file, you are effectively compiling similar code into two different crates. That can be valid, but it often causes confusion.

Avoid this unless you intentionally want separate crate trees.

4. Misunderstanding crate::

Broken mental model:

  • crate:: means the whole Cargo package

Correct mental model:

  • crate:: means the current crate only

5. Calling library code without crate name from main.rs

Broken code:

fn main() {
    bar::bar();
}

This only works if bar is a module in the binary crate.

If bar is defined in lib.rs, use the library crate name:

fn main() {
    hello_world::bar::bar();
}

6. Thinking mod and use do the same thing

They do different jobs:

  • mod declares and loads a module into the current crate
  • use brings a path into local scope

Example:

mod foo;      // defines module in this crate
use crate::foo; // imports that path into this scope

Comparisons

ConceptWhat it doesApplies toExample
modDeclares a module as part of the current crateCurrent crate onlymod foo;
pub modDeclares a module and makes it publicCurrent crate onlypub mod foo;
useBrings an existing path into scopeCurrent module scopeuse crate::foo;
crate::Refers to the root of the current crateCurrent crate onlycrate::foo::say_foo()
library crate nameRefers to the separate library crate from another crateExternal or sibling crate usagehello_world::bar::bar()

main.rs vs lib.rs

FileRoleCrate typeTypical purpose
src/main.rsEntry point for executableBinary crateStart program, parse args, call library code
src/lib.rsRoot of reusable codeLibrary crateExpose modules, logic, API

When to use each approach

ApproachGood forDrawback
Declare modules in main.rsSmall binary-only programsHarder to reuse and test
Declare modules in lib.rs and call them from main.rsMost reusable project layoutsRequires understanding separate crates
Declare modules in bothRare special casesEasy to confuse module paths
Take Quiz

Cheat Sheet

Core rule

main.rs and lib.rs are separate crate roots.

What crate:: means

crate::something

Means: start from the root of the current crate.

Module declaration

mod foo;
  • declares foo in the current crate
  • usually loads code from foo.rs or foo/mod.rs
pub mod foo;
  • same as mod foo;
  • also makes the module public

Importing

use crate::foo;
  • only works if foo exists in the current crate root

Binary crate pattern

// src/main.rs
fn main() {
    my_crate::run();
}

Library crate pattern

// src/lib.rs
pub mod foo;

pub fn run() {
    foo::do_work();
}

Your error in one line

bar.rs was compiled under main.rs, but main.rs did not declare foo, so crate::foo did not exist.

Best practice

  • put reusable logic in lib.rs
  • keep main.rs small
  • call library code from main.rs

Quick fix options

Option 1: Binary-only layout

// main.rs
mod foo;
mod bar;

Option 2: Preferred shared layout

// lib.rs
pub mod foo;
pub mod bar;
// main.rs
fn main() {
    hello_world::bar::bar();
}

FAQ

Why does crate::foo fail even though foo exists in lib.rs?

Because crate:: refers to the current crate only. If bar.rs is compiled from main.rs, then crate means the binary crate, not the library crate.

Does lib.rs automatically expose modules to main.rs?

No. lib.rs defines the library crate's module tree. main.rs is a separate binary crate.

Why did adding mod foo; to main.rs fix the problem?

Because it created foo inside the binary crate, so crate::foo became valid there.

What is the best way to organize code when I have both main.rs and lib.rs?

Usually, put reusable logic in lib.rs and keep main.rs as a small entry point that calls into the library.

Can a package have both a binary crate and a library crate?

Yes. That is very common in Rust.

Should I declare the same modules in both main.rs and lib.rs?

Usually no. It can work, but it often creates confusion because they belong to different crate trees.

How does main.rs access code from lib.rs?

By using the library crate name, which usually matches the package name from Cargo.toml, such as hello_world::foo::say_foo().

Is use enough to make a module exist?

No. use imports a path that already exists. mod declares the module in the current crate.

Take Quiz

Mini Project

Description

Build a small Rust package that separates reusable logic into a library crate and uses a binary crate to run it. This demonstrates the correct relationship between lib.rs, main.rs, modules, and crate:: paths.

Goal

Create a package where main.rs calls a public function from the library crate, and one library module calls another using crate::....

Requirements

  • Create a foo module with a public function that returns or prints a message.
  • Create a bar module that uses crate::foo internally.
  • Expose both modules from lib.rs.
  • Call the bar functionality from main.rs using the library crate name.
Take Quiz

Keep learning

Related questions

View more