3 min read
On this page

Project Structure

Rust's module system controls how code is organized, what is public, and how names are resolved. It is different from most languages — there is no filesystem-based auto-discovery. You explicitly declare your module tree, and the compiler enforces visibility rules at compile time.

The Basics: src/main.rs & src/lib.rs

Every Rust project starts with one of two root files:

  • src/main.rs — binary crate, produces an executable
  • src/lib.rs — library crate, produces a reusable library

A project can have both. When it does, src/main.rs can use the library crate by importing it with the crate name from Cargo.toml:

my-project/
  Cargo.toml
  src/
    main.rs    # binary crate root
    lib.rs     # library crate root
// src/lib.rs
pub fn process(input: &str) -> String {
    input.to_uppercase()
}

// src/main.rs
use my_project::process;

fn main() {
    println!("{}", process("hello"));
}

This split is common: put all logic in lib.rs (and its modules), keep main.rs thin. This lets you test the library crate independently and reuse it from other binaries.

Modules as Files vs Directories

There are two ways to define a module in a separate file.

Single-file module

src/
  lib.rs
  config.rs
// src/lib.rs
mod config;  // loads src/config.rs

pub use config::Settings;
// src/config.rs
pub struct Settings {
    pub port: u16,
    pub host: String,
}

Directory module

When a module has submodules, use a directory with a mod.rs file:

src/
  lib.rs
  db/
    mod.rs
    connection.rs
    query.rs
// src/lib.rs
mod db;

// src/db/mod.rs
mod connection;
mod query;

pub use connection::Pool;
pub use query::execute;

Alternatively, since Rust 2018, you can use a file named after the directory instead of mod.rs:

src/
  lib.rs
  db.rs          # same role as db/mod.rs
  db/
    connection.rs
    query.rs

Both approaches work. The db.rs style avoids multiple mod.rs tabs in your editor. Pick one and stay consistent.

mod, pub & use

mod declares a module

mod auth;       // declares the auth module, loads from auth.rs or auth/mod.rs
mod auth {      // inline module — rarely used except in tests
    pub fn login() {}
}

pub controls visibility

By default, everything is private. pub makes it visible to the parent module and beyond:

mod server {
    pub struct Config {
        pub port: u16,
        host: String,  // private — only accessible within this module
    }

    impl Config {
        pub fn new(port: u16, host: String) -> Self {
            Config { port, host }
        }

        pub fn host(&self) -> &str {
            &self.host
        }
    }
}

fn main() {
    let config = server::Config::new(8080, "localhost".into());
    println!("Port: {}", config.port);
    // println!("{}", config.host);  // ERROR: host is private
    println!("Host: {}", config.host());  // OK: public method
}

pub(crate) makes something visible within the crate but not to external consumers — useful for internal helpers:

pub(crate) fn internal_helper() -> u32 {
    42
}

use brings names into scope

use std::collections::HashMap;
use std::io::{self, Read, Write};  // grouped imports

use crate::db::Pool;               // absolute path from crate root
use super::config::Settings;       // relative path from parent module

Re-exports with pub use

pub use is how you curate your crate's public API. Internal organization can be deep; the public surface should be flat:

// src/lib.rs
mod domain;
mod infrastructure;

// Users of your crate see these at the top level
pub use domain::user::User;
pub use domain::order::Order;
pub use infrastructure::db::connect;

Now consumers write use your_crate::User instead of use your_crate::domain::user::User. This is critical for library crates — your module structure is an implementation detail, not an API.

The Module Tree

Rust's module system forms a tree rooted at lib.rs or main.rs. Every module has exactly one parent (except the root). The compiler resolves paths by walking this tree:

crate (lib.rs)
  ├── domain
  │   ├── user
  │   └── order
  ├── infrastructure
  │   ├── db
  │   └── cache
  └── application
      └── handlers

Path resolution:

  • crate::domain::user::User — absolute from root
  • self::helper() — current module
  • super::other_module::func() — parent module

Organizing a Real Project

A small project can get away with a flat module structure. Once a project grows past a few thousand lines, consider organizing by domain layer:

src/
  lib.rs
  main.rs
  domain/
    mod.rs
    user.rs
    order.rs
    error.rs
  infrastructure/
    mod.rs
    db.rs
    cache.rs
    email.rs
  application/
    mod.rs
    handlers.rs
    middleware.rs
  config.rs
// src/lib.rs
pub mod domain;
pub mod infrastructure;
pub mod application;
pub mod config;

// Re-export the most-used types
pub use config::AppConfig;
pub use domain::error::AppError;
// src/domain/mod.rs
pub mod user;
pub mod order;
pub mod error;

Domain modules contain business types and validation. Error modules define your crate's error enum with Display and Error implementations.

Guidelines for layer organization

  • domain/ — business logic, types, validation. No dependencies on infrastructure.
  • infrastructure/ — database, HTTP clients, file system, external services.
  • application/ — glue code that wires domain logic to infrastructure. Handlers, middleware, orchestration.
  • config.rs — configuration parsing, environment variables.

This is not a hard rule — it is a starting point. The key principle is that domain code should not depend on infrastructure code. Dependencies flow inward.

Common Pitfalls

  • Forgetting mod declarations — creating a file does not add it to the module tree. You must declare mod filename; in the parent module.
  • Circular dependencies — Rust does not allow circular module dependencies. If A needs B and B needs A, extract the shared types into a third module.
  • Over-nesting — five levels of nested modules makes paths painful. Flatten with pub use.
  • Everything pub — making everything public defeats the purpose of the module system. Start private, expose only what consumers need.
  • Confusing mod.rs with lib.rsmod.rs is the root of a sub-module, not a crate root. It does not get special treatment from Cargo.

Key Takeaways

  • src/main.rs is the binary root, src/lib.rs is the library root. Use both to keep main.rs thin.
  • mod declares modules. pub controls visibility. use brings names into scope.
  • pub use re-exports create a clean public API independent of internal structure.
  • Organize by domain layer once a project grows: domain, infrastructure, application.
  • Start private, expose deliberately. The module system is your tool for enforcing API boundaries.