Rust’s module system is surprisingly confusing and causes a lot of frustration for beginners.

In this post, I’ll explain the module system using practical examples so you get a clear understanding of how it works and can immediately start applying this in your projects.

Since Rust’s module system is quite unique, I request the reader to read this post with an open mind and resist comparing it with how modules work in other languages.

Let’s use this file structure to simulate a real world project:

my_project
├── Cargo.toml
└─┬ src
  ├── main.rs
  ├── config.rs
  ├─┬ routes
  │ ├── health_route.rs
  │ └── user_route.rs
  └─┬ models
    └── user_model.rs

These are the different ways we should be able to consume our modules:

These 3 examples should be sufficient to explain how Rust’s module system works.

Example 1

Let’s start with the first example - importing config.rs in main.rs.

// main.rs
fn main() {
  println!("main");
}
// config.rs
fn print_config() {
  println!("config");
}

The first mistake that everyone makes is just because we have files like config.rs, health_route.rs etc, we think that these files are modules and we can import them from other files.

Here’s what we see (file system tree) and what the compiler sees (module tree):

Surprisingly, the compiler only sees the crate module which is our main.rs file. This is because we need to explicitly build the module tree in Rust - there’s no implicit mapping between file system tree to module tree.

We need to explicitly build the module tree in Rust, there’s no implicit mapping to file system

To add a file to the module tree, we need to declare that file as a submodule using the mod keyword. The next thing that confuses people is that you would assume we declare a file as module in the same file. But we need to declare this in a different file! Since we only have main.rs in the module tree, let’s declare config.rs as a submodule in main.rs.

The mod keyword declares a submodule

The mod keyword has this syntax: