13.4Splitting code into multiple files

Last updated June 13, 2026

Here's the lesson the chapter has been building toward, the one that replaces six lessons of C++ header machinery (#include, header guards, declaration-versus-definition splits, the preprocessor) with a single idea. Once a module grows past a screenful, you'll want it in its own file. In Rust, moving it there is almost nothing: declare the module, and put its body in a file named after it.

From inline module to file

Start with everything in main.rs, the way the last three lessons wrote it:

// src/main.rs
mod hosting {
    pub fn add_to_waitlist() {
        println!("added to waitlist");
    }
}

fn main() {
    hosting::add_to_waitlist();
}

To move hosting into its own file, do two things. First, in main.rs, replace the module's body with a semicolon:

// src/main.rs
mod hosting;

fn main() {
    hosting::add_to_waitlist();
}

mod hosting; (with a semicolon, no braces) tells the compiler: "there's a module called hosting; its contents are in another file, go find them." Second, create that file, src/hosting.rs, and put the body in it:

// src/hosting.rs
pub fn add_to_waitlist() {
    println!("added to waitlist");
}

That's the whole operation. mod hosting; in the parent, hosting.rs holding the contents. The compiler looks for src/hosting.rs (or, as we'll see, src/hosting/mod.rs) automatically based on the module name. Everything else, the paths, use, pub, the module tree from the last three lessons, works identically. Splitting into files changes where the code physically lives, not how it's organized or referred to.

Key insight

mod hosting; is a declaration of where to find a module, not a textual include. The compiler reads hosting.rs once, as a module, with its own scope and privacy. Compare C++'s #include "hosting.h", which literally pastes a file's text into yours and so needs header guards to avoid pasting twice, separate declaration and definition files, and forward declarations to break cycles. Rust has none of that, because mod names a module rather than splicing text. The mental model is "this module's body is over there," full stop.

Directory modules

When a module itself contains submodules, it grows beyond one file and wants a directory. Say front_of_house contains hosting and serving. You make a directory named front_of_house, and Rust offers two conventions for the directory's own code:

The modern style uses a file named after the module next to the directory:

src/
├── main.rs
├── front_of_house.rs        ← front_of_house's own code + `pub mod hosting;`
└── front_of_house/
    ├── hosting.rs
    └── serving.rs
// src/main.rs
mod front_of_house;

// src/front_of_house.rs
pub mod hosting;
pub mod serving;

// src/front_of_house/hosting.rs
pub fn add_to_waitlist() { /* ... */ }

main.rs declares mod front_of_house;, found in front_of_house.rs. That file in turn declares pub mod hosting; and pub mod serving;, which the compiler finds inside the front_of_house/ directory. The module tree is unchanged; only the file layout reflects the nesting now.

The older style puts the module's code in a file called mod.rs inside the directory (src/front_of_house/mod.rs) instead of front_of_house.rs beside it. Both work and you'll see both in the wild. The newer front_of_house.rs style is generally preferred because a project full of files all named mod.rs is hard to navigate in an editor.

Best practice

Prefer the front_of_house.rs-beside-the-directory style over mod.rs-inside-it. Editors show you a dozen distinct file names instead of a dozen tabs all reading mod.rs. Either way, let the module structure drive the file structure: one module per file, nested modules as directories, and the paths in your code stay the same regardless of how you split.

What didn't change

It's worth dwelling on how little the move disturbed. The call hosting::add_to_waitlist() is byte-for-byte the same before and after splitting. pub still controls visibility, use still shortens paths, super:: and crate:: still navigate the same tree. There was no header to write, no declaration to keep in sync with a definition, no build-system file to edit listing the new source. You added one mod line and one file. This is the selling point stated plainly: organizing a large Rust project across many files is the same as organizing it within one, plus a mod declaration per file.

Quiz time

Question #1

You have an inline module mod parser { pub fn parse() {} } in main.rs and want it in its own file. What two changes do you make?

Show solution

(1) In main.rs, replace the module body with a declaration: mod parser; (semicolon, no braces). (2) Create src/parser.rs containing the body: pub fn parse() {}. The compiler finds parser.rs from the mod parser; line. No other change is needed; call sites stay the same.

Question #2

What does mod parser; (with a semicolon) tell the compiler, and how does it differ from C++'s #include?

Show solution

It declares that a module named parser exists and that its contents live in another file (parser.rs or parser/mod.rs), which the compiler locates by name. It is not a textual paste: the file is read once as a proper module with its own scope and privacy. C++'s #include literally copies a file's text into the current one, which is why it needs header guards, separate declaration/definition, and forward declarations. mod needs none of those.

Question #3

A module network needs submodules tcp and udp. Sketch a valid file/directory layout using the preferred style.

Show solution
src/
├── main.rs            (or lib.rs): contains `mod network;`
├── network.rs         contains `pub mod tcp;` and `pub mod udp;`
└── network/
    ├── tcp.rs
    └── udp.rs

network.rs sits beside the network/ directory and holds network's own code plus the submodule declarations; the submodule files live inside the directory. (The mod.rs alternative would put network's code in src/network/mod.rs instead.)

Files solved, the next two lessons zoom out to the package level: the difference between a binary crate and a library crate (and why having both matters for testing), then a tour of what Cargo does beyond cargo run.