When the file name doesn't match the module name
You are refactoring a Rust project. Your lib.rs has grown to 400 lines. You decide to split the configuration logic into its own module. You type mod config; at the top of lib.rs. You create src/config.rs. You move the code. You run cargo build. It compiles. You feel productive.
Then you clone a repository from a senior developer. You look at their structure. Their config module lives in src/config/mod.rs. You try to copy their pattern. You create src/config/mod.rs. It also compiles. Now you have two ways to do the same thing. You suspect one is wrong. You aren't. Both work. But one is the modern standard, and the other is a relic from a time when Rust's module system felt like a puzzle box.
The confusion comes from a change in Rust 2018. Before that edition, mod.rs was mandatory for directory-based modules. The file name mod.rs told the compiler "this folder is a module." The folder name didn't matter to the compiler, only the file name inside it. This broke the intuition that module names should match file names. Rust 2018 fixed this. Now the compiler looks for name.rs or name/mod.rs when you declare mod name;. The mod.rs file is optional. You can use a flat file that matches the module name, or a directory with a mod.rs inside. The flat file is the default for new code.
The module system: declarations vs definitions
Rust separates the declaration of a module from its definition. The mod keyword does both, but the syntax changes based on whether you include a semicolon.
mod name; is a declaration. It tells the compiler that a module named name exists, but the code isn't here. The compiler must find the definition in a file.
mod name { ... } is an inline definition. The code lives right there in the braces. No file is involved.
When you use the semicolon form, the compiler runs a resolution algorithm. It looks for a file that matches the module name. In Rust 2018 and later, the search order is strict. The compiler checks for name.rs first. If that file exists, it uses it. If not, it checks for name/mod.rs. If that exists, it uses the directory structure. If neither exists, the compiler emits an error.
This resolution happens relative to the file containing the declaration. If lib.rs declares mod utils;, the compiler looks in src/. If src/utils.rs declares mod helper;, the compiler looks in src/utils/. The path is always relative to the current module's file, not the crate root. This keeps the module tree predictable. You can move a module and its submodules to a different directory, and as long as the relative structure stays the same, the code compiles.
Think of the module tree as a shadow of the file tree. Every mod declaration casts a shadow relative to its location. The compiler never jumps back to the crate root unless you explicitly use crate:: paths. This relative anchoring prevents name collisions and makes refactoring safe. You can rename a directory and update the parent declaration, and all submodules follow along automatically.
Minimal example: the modern way
The preferred structure for a new module is a single file named after the module. This keeps the project flat and readable.
Here's the smallest case: a declaration and a matching file.
// src/lib.rs
/// Declare the utils module.
/// The compiler searches for src/utils.rs or src/utils/mod.rs.
mod utils;
/// Re-export the helper function at the crate root.
pub use utils::format_name;
// src/utils.rs
/// String formatting utilities.
/// This file matches the module name declared in lib.rs.
pub fn format_name(name: &str) -> String {
// Return the name in uppercase with a prefix.
format!("User: {}", name.to_uppercase())
}
The file src/utils.rs defines the utils module. The name matches exactly. There is no mod.rs. There is no directory. The compiler finds utils.rs immediately. This is the pattern you should use for 90% of your modules. It avoids nesting and keeps the file explorer clean.
Convention aside: The community calls this the "flat module" pattern. When you see a Rust project with a shallow src/ directory full of .rs files, that's the modern style. It signals that the author values simplicity and hasn't over-engineered the hierarchy.
Trust the flat file. If the module fits in one file, keep it there.
How the compiler resolves paths
Understanding the resolution order prevents subtle bugs. The compiler does not guess. It follows the rules.
When you write mod foo; inside src/bar.rs, the compiler looks for src/bar/foo.rs or src/bar/foo/mod.rs. It does not look for src/foo.rs. The declaration is relative to bar.rs. This is a common tripwire for beginners. You might expect mod foo; to always look in the crate root. It doesn't. The module tree mirrors the file tree relative to each declaration.
If you have mod foo; in lib.rs and mod foo; in src/bar.rs, you have two different modules named foo. One lives at the crate root. The other lives inside bar. They can have different code. The compiler treats them as distinct namespaces. This allows you to reuse module names in different contexts without conflict.
The resolution also handles the mod.rs legacy case. If you have src/foo/mod.rs but no src/foo.rs, the compiler uses the directory. The mod.rs file becomes the root of the foo module. Any mod declarations inside mod.rs resolve relative to src/foo/. This supports deep hierarchies. You can have src/foo/bar.rs as a submodule of foo. The structure scales.
Convention aside: When using a directory structure, the community prefers name.rs over name/mod.rs even for directories. If you need submodules, create name.rs as the entry point and put submodules in name/. This avoids the "magic" mod.rs file entirely. The directory name name already tells you the module name. The mod.rs file adds no information.
Keep the resolution path explicit. If the compiler has to search for mod.rs, you're adding cognitive load for zero gain.
Realistic example: nested modules
Large projects often need nested modules. A database crate might have connection, query, and migration submodules. The directory structure helps organize these.
Here's a realistic structure: a parent module splitting work into submodules.
// src/lib.rs
/// Database interaction crate.
/// Declares the db module. The compiler finds src/db.rs.
mod db;
// src/db.rs
/// Database module root.
/// Splits functionality into connection and query submodules.
pub mod connection;
pub mod query;
The db.rs file acts as the entry point. It declares connection and query. The compiler looks for src/db/connection.rs and src/db/query.rs.
Here's the connection submodule defining its types.
// src/db/connection.rs
/// Manages database connections.
/// Lives in the db directory because db.rs declares it.
pub struct Connection {
/// Host address for the database server.
pub host: String,
}
impl Connection {
/// Create a new connection to the host.
pub fn new(host: String) -> Self {
// Return a connection with the given host.
Connection { host }
}
}
The structure is clean. You could replace src/db.rs with src/db/mod.rs and the code would compile identically. The community prefers db.rs because it avoids the extra mod.rs file. The directory name db already tells you the module name. The mod.rs file adds no information.
Convention aside: Some teams use mod.rs when a module has no public interface of its own and only exists to group submodules. This is a rare edge case. In 99% of projects, name.rs is the right choice.
Keep the flat file. The directory name carries the weight.
Pitfalls and compiler errors
The compiler catches most mistakes, but the error messages can be confusing if you don't know the resolution rules.
If you name the file wrong, the compiler rejects the build. You declare mod utils; but create src/util.rs. The compiler looks for utils.rs. It doesn't find it. It emits E0583 (file not found for module utils). The error lists the paths the compiler checked. You see src/utils.rs and src/utils/mod.rs in the output. You realize the typo. You rename the file. The error disappears.
A more subtle issue is the conflict between name.rs and name/mod.rs. If you have both src/db.rs and src/db/mod.rs, the compiler picks db.rs and ignores the directory. It does not error. It silently prefers the flat file. This can hide bugs. You might think you're using the directory structure, but the compiler is using the flat file. If you delete db.rs expecting the directory to take over, the build breaks. The compiler emits E0583 again because the flat file is gone and the directory might not match expectations. Keep only one form. If you use db.rs, delete the db directory. If you use db/mod.rs, delete db.rs.
Another pitfall is forgetting pub. Modules are private by default. You declare mod utils; in lib.rs. You define pub fn helper() in utils.rs. You try to call crate::utils::helper() from another crate. It fails. The utils module is private. You need pub mod utils; in lib.rs. The compiler emits E0603 (module utils is private). The function is public, but the module isn't. You fix the declaration. The error clears. Visibility propagates up. A public function inside a private module is unreachable from outside.
Convention aside: When you see E0603, check the module declaration first. Beginners often fix the function visibility and forget the module. The module is the gatekeeper. If the gate is closed, the function doesn't matter.
A public function inside a private module is unreachable. Fix the module visibility, not the function.
Inline modules vs file modules
Inline modules are useful for small pieces of code that don't warrant a file. You can define a module right inside the current file.
Here's how to define a module without touching the file system.
// src/lib.rs
/// Configuration module defined inline.
/// No separate file is needed for this small module.
mod config {
/// Default port for the server.
pub const DEFAULT_PORT: u16 = 8080;
/// Load configuration from environment variables.
pub fn load() -> String {
// Return a placeholder config string.
"config_from_env".to_string()
}
}
This config module lives entirely in lib.rs. You can access it via crate::config::DEFAULT_PORT. The compiler doesn't look for a file. The code is self-contained. This is great for grouping related constants or helper functions that are tightly coupled to the parent module.
You can mix inline and file modules. lib.rs can have mod config { ... } and mod utils;. The config module is inline. The utils module is in a file. The compiler handles both seamlessly. You can even nest them. utils.rs can contain mod internal { ... } inline while also declaring mod helper; for a file-based submodule. The flexibility lets you choose the right granularity for each piece of code.
Convention aside: The community tends to use inline modules sparingly. If a module grows beyond a few lines, moving it to a file improves readability. Files act as boundaries. They force you to think about the module's interface. Inline modules can encourage dumping code into the parent file. Use inline modules for tiny, self-contained groups. Use files for anything that needs to breathe.
Inline modules are for grouping, not dumping. Move to a file when the module needs to breathe.
Decision: which structure to pick
Use name.rs when the module fits in a single file. This is the default for almost all new modules. It keeps the project flat and matches the module name to the file name.
Use name/mod.rs when the module is large enough to warrant a directory, and you need to split it into submodules like name/sub.rs. This structure scales for complex hierarchies.
Use inline mod name { ... } when the module is tiny and tightly coupled to the parent file. This avoids file clutter for small groups of constants or helpers.
Use mod.rs in legacy codebases only when maintaining projects that haven't migrated to the 2018 edition. Do not create new mod.rs files unless you have a specific reason to support the old resolution rules.
Keep the file name identical to the module name. The compiler expects it. If the module is utils, the file is utils.rs. No surprises. If you see mod.rs in a tutorial, check the date. You are likely watching a video from before 2018. Trust the modern structure. It is simpler and less error-prone.