Fundamentals 10 min read

Master Rust Module Organization: Simple Patterns for Cross-File Imports

This tutorial walks Rust beginners through three practical ways to declare and use modules across files, explains Cargo's file‑placement conventions, and shows how to reference sibling and external modules with the `use` and `super` keywords.

ELab Team
ELab Team
ELab Team
Master Rust Module Organization: Simple Patterns for Cross-File Imports

This article is aimed at beginners learning Rust and helps understand how Rust modules reference each other, using minimal code so even readers with no prior Rust experience can get familiar.

This article is suitable for beginners learning Rust, to help understand how Rust modules reference each other. It uses as little code as possible so even without prior Rust knowledge you can get a feel for it.

If you are new to Rust, you only need to know that Cargo is the dependency‑management tool similar to npm, and that Cargo follows a file‑placement convention where the file name becomes the module name. A crate is a package of functionality, similar to an npm package.

The scenario discussed is splitting code into multiple files and extracting modules, which often requires importing part of another module’s code. In JavaScript this is done with import/export paths; in Rust modules are built via Cargo and declared in a module tree, then used with the use keyword.

Module Declaration & Usage

Suppose we want to implement an addition module and expose it to other parts of the project. Three organization methods are presented.

Cargo uses file‑placement conventions, so module lookup is based on .rs files or directories under src . Only the first level is searched; nested directories cannot be directly used by other files.

Method 1: Declare directly in the root file (add.rs)

By adding a file with the same name as the module under src, Cargo can recognize the add module.

├── Cargo.lock
├── Cargo.toml
├── src
│   ├── add.rs
│   ├── lib.rs

Method 2: Declare an add folder with mod.rs

If the module is a folder, it must contain a mod.rs file. This is similar to JavaScript’s index.js. Cargo will still recognize it as the add module.

├── Cargo.lock
├── Cargo.toml
├── src
│   ├── add
│   │   ├── mod.rs
│   ├── lib.rs

Assume the code content is:

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

In lib.rs we can call the add module as follows:

// Declare module and re‑export its function
mod add;
pub use crate::add::add_fn;

pub fn test_lib() {
    add_fn(1, 2);
}

Method 3: Both add.rs and an add folder coexist

The directory structure looks like this:

├── Cargo.lock
├── Cargo.toml
├── src
│   ├── add
│   │   └── add_m.rs
│   ├── add.rs // entry module
│   ├── lib.rs
add.rs

acts as the entry point, re‑exporting sub‑modules, similar to JavaScript’s index.js that aggregates multiple modules. The mod keyword is used to split modules.

pub mod add_m;
// similar to export * from './validate'; export * from './helper'

The sub‑module file add/add_m.rs contains the function:

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

In lib.rs we use:

mod add;
pub use crate::add::add_m::add_fn;

pub fn test_lib() {
    add_fn(1, 2);
}

In practice the first two methods are most common; the second method is often preferred for larger projects because it better organizes files.

Same‑module adjacent file references

We adjust the directory structure as follows:

├── Cargo.lock
├── Cargo.toml
├── src
│   ├── add
│   │   ├── mod.rs
│   │   ├── print.rs
│   │   └── user.rs // user calls print's method
│   ├── lib.rs
print.rs

defines a module with a public function:

pub mod my_print {
    pub fn print_hello() {
        println!("hello");
    }
}
// The `pub mod` can be loosely understood as a TypeScript `declare module`
user.rs

imports the sibling module using super:

use super::print::my_print;

pub fn hello_user() {
    my_print::print_hello();
}

pub struct User {
    pub name: String,
}

The mod.rs file declares the modules and re‑exports them:

// mod.rs is the entry file; `mod` looks for a same‑name file or folder
mod print;
mod user;

pub use self::user::hello_user;
pub use self::user::User;

pub mod add_fn {
    pub fn add(a: i32, b: i32) -> i32 {
        // Call a function from another sibling module
        super::hello_user();
        let value = super::User { name: String::from("Rust") };
        println!("user name {}", value.name);
        a + b
    }
}

pub fn test_out_ref() {
    hello_user();
}

Different module references

We add a new module multip that returns the product of two numbers:

pub fn res_multip(a: i32, b: i32) -> i32 {
    a * b
}

In add ’s mod.rs we import it:

use crate::multi::multip;

Now the add module can use functions from the multip module, and the same pattern applies to other modules.

Summary

Rust’s module system is relatively straightforward, but the official documentation does not provide detailed guidance on splitting and organizing modules, which can be unfamiliar to developers transitioning from JavaScript. This article clarifies common patterns for declaring, exposing, and cross‑referencing modules, helping readers structure Rust projects more effectively.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

RustmoduleCode Organizationimportcargo
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.