$elementary.dev
[Courses][About]|
$elementary.dev

Text-first, project-driven courses for developers who want to level up by building real things.

CoursesAbout
Stay Updated

Get notified about new courses and articles.

// No spam, unsubscribe anytime

© 2026 elementary.dev
PrivacyTerms
// Course

Elementary Rust

10/11
  • 01Hello, Rust
  • 02Variables and Types
  • 03Functions and Control Flow
  • 04Why Memory Management Matters
  • 05Ownership: Rust's Solution
  • 06Borrowing and References
  • 07Slices
  • 08Structs
  • 09Enums and Pattern Matching
  • 10Organizing Rust Projects
  • 11Collections
courses/elementary-rust/10-organizing-code

Organizing Rust Projects

Structure your code with packages, crates, and modules.

~20 min|#Lesson 10/11

Organizing Rust Projects

As your Rust programs grow, you need ways to organize code. Rust gives you three tools: packages, crates, and modules. Let's break them down.

// Info-What you'll learn

By the end of this lesson, you'll understand how Rust projects are structured, how to split code across files, and why everything is private by default.

Crates: The Building Block

// A crate is the smallest unit of code the compiler considers
// Two types exist:

// Binary crate: has main(), compiles to executable
fn main() {
    println!("I'm a binary crate!");
}

// Library crate: no main(), defines functionality for others
pub fn do_something() {
    // Other crates can use this
}

A crate is the smallest unit of code the Rust compiler considers at once. Think of it as a tree of modules that produces either an executable or a library.

There are exactly two types:

  • Binary crate: Has a main() function, compiles to an executable you can run
  • Library crate: No main(), defines functionality for other crates to use

Most of the time when Rustaceans say "crate," they mean library crate. You'll hear "crate" used interchangeably with "library."

The crate root is the entry file. For binary crates, it's src/main.rs. For library crates, it's src/lib.rs. The compiler starts here and builds everything.

// Tip-One library, many binaries

A package can have at most one library crate but unlimited binary crates. This is great for projects like CLI tools that share a core library.

Packages: Bundling Crates Together

# Cargo.toml defines a package
[package]
name = "my_project"
version = "0.1.0"
edition = "2021"

A package is a bundle of one or more crates with a Cargo.toml. When you run cargo new my_project, you get a package.

The rules are simple:

  • Must have at least one crate
  • At most ONE library crate
  • Any number of binary crates

Here's a typical project structure:

my_project/
├── Cargo.toml        # Package definition
├── src/
│   ├── main.rs       # Default binary crate root
│   ├── lib.rs        # Library crate root (optional)
│   └── bin/
│       ├── server.rs # Additional binary
│       └── client.rs # Additional binary

Each file in src/bin/ becomes a separate binary crate. Run them with cargo run --bin server or cargo run --bin client.

// Challenge

Quick Check: Crate Types

What file is the crate root for a library crate?

$

Modules: Organizing Within a Crate

// src/main.rs
mod garden;  // Declares a module, looks for src/garden.rs

fn main() {
    garden::harvest();
}
// src/garden.rs
pub fn harvest() {
    println!("Harvesting vegetables!");
}

Modules let you organize code within a crate. Declare them with the mod keyword.

When you write mod garden;, Rust looks for the module code in two places:

  1. src/garden.rs (modern style, preferred)
  2. src/garden/mod.rs (older style)

Both work, but prefer garden.rs over garden/mod.rs. With many modules, you'll end up with many files named mod.rs and your editor tabs become useless.

Nested Modules

src/
├── main.rs
├── garden.rs          # mod garden
└── garden/
    └── vegetables.rs  # mod vegetables (inside garden)
// src/main.rs
mod garden;

fn main() {
    garden::vegetables::pick();
}
// src/garden.rs
pub mod vegetables;  // Looks for src/garden/vegetables.rs
// src/garden/vegetables.rs
pub fn pick() {
    println!("Picking vegetables!");
}

For nested modules, Rust looks in a subdirectory matching the parent module name. The file src/garden/vegetables.rs becomes the garden::vegetables module.

// Warning-Modern vs older style

The older mod.rs style puts everything in garden/mod.rs instead of garden.rs. Both compile the same way, but garden.rs is cleaner. Stick with the modern style.

Everything is Private by Default

// src/main.rs
mod garden;

fn main() {
    // This works - garden and vegetables are public
    garden::vegetables::Asparagus::new();
}
// src/garden.rs
pub mod vegetables;  // pub makes it accessible outside garden
// src/garden/vegetables.rs
pub struct Asparagus {
    pub height: u32,  // pub on each field too!
    soil_type: String, // private - not accessible outside
}

impl Asparagus {
    pub fn new() -> Self {
        Asparagus {
            height: 0,
            soil_type: String::from("loam"),
        }
    }
}

This is crucial: everything in Rust is private by default. To expose something, add pub at EVERY level:

  1. The module itself (pub mod vegetables)
  2. Items inside the module (pub struct Asparagus)
  3. Struct fields (pub height: u32)

Miss any level and external code can't access it. This is intentional. Rust makes you explicitly choose what's part of your public API.

// Info-The privacy philosophy

Private-by-default means you can refactor internals freely. Only public items are promises to external users. Change private code without breaking anyone.

Paths: Navigating Your Code

// Absolute path - starts from crate root
crate::garden::vegetables::pick();

// Relative path - starts from current module
garden::vegetables::pick();

// Super - go up to parent module
super::sibling_function();

Two ways to reference items:

Absolute paths start with crate:: and work from the crate root. Use these when the definition and usage might move independently.

Relative paths start from the current module. Use super to go up one level, like .. in a filesystem. You can chain super to go up multiple levels: super::super:: goes up two levels.

mod back_of_house {
    pub fn fix_order() {
        // Go up to parent, then into front_of_house
        super::front_of_house::serve_order();
    }
}

mod front_of_house {
    pub fn serve_order() {}
}

Choose based on what makes sense for your code. Absolute paths are longer but more explicit. Relative paths are shorter but break if you move the calling code.

The use Keyword

// Without use - verbose
fn eat() {
    crate::front_of_house::hosting::add_to_waitlist();
    crate::front_of_house::hosting::seat_at_table();
    crate::front_of_house::hosting::add_to_waitlist();
}

// With use - clean
use crate::front_of_house::hosting;

fn eat() {
    hosting::add_to_waitlist();
    hosting::seat_at_table();
    hosting::add_to_waitlist();
}

The use keyword brings paths into scope so you don't repeat yourself.

There's an idiom here:

  • For functions: Bring the parent module (use hosting), then call hosting::add_to_waitlist(). This makes it clear the function isn't locally defined.
  • For structs and enums: Bring the type directly (use HashMap), then use HashMap::new().
// Functions - bring parent module
use std::collections::hash_map;
let map = hash_map::HashMap::new();  // Clear it's from hash_map

// Structs/enums - bring the type
use std::collections::HashMap;
let map = HashMap::new();  // More idiomatic for types
// Tip-The exception

When two types have the same name, bring their parent modules to disambiguate. You can't have two Result types in scope without qualification.

Aliases with as

use std::fmt::Result;
use std::io::Result as IoResult;

fn function1() -> Result {
    Ok(())
}

fn function2() -> IoResult<()> {
    Ok(())
}

When names collide, rename one with as. This keeps both accessible without full paths.

Re-exporting with pub use

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

// Re-export for external users
pub use crate::front_of_house::hosting;

// Now external code can use:
// my_crate::hosting::add_to_waitlist()
// Instead of:
// my_crate::front_of_house::hosting::add_to_waitlist()

pub use re-exports an item at a different location. Your internal structure might be deep, but you can expose a flat public API.

This is powerful for library design. Organize internally however makes sense, then present a clean interface to users.

Nested Paths

// Before - repetitive
use std::cmp::Ordering;
use std::io;

// After - nested
use std::{cmp::Ordering, io};

// Bring a module and something inside it
use std::io::{self, Write};
// self means std::io itself, plus std::io::Write

Nest paths with curly braces to reduce boilerplate. The self keyword refers to the path before the braces.

You can also use the glob operator to bring everything from a module into scope:

use std::collections::*;

Use glob sparingly. It makes it harder to tell where names come from.

Structs vs Enums: A Privacy Difference

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,      // Public - user chooses
        seasonal_fruit: String, // Private - chef chooses
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }

    pub enum Appetizer {
        Soup,    // Public because enum is public
        Salad,   // Also public
    }
}

Structs and enums have different privacy rules:

  • Struct fields are private by default. Mark each field pub individually. If any field is private, external code can't construct the struct directly (need a constructor function).
  • Enum variants are all public if the enum is public. It doesn't make sense to have a private variant you can't match against.
// Warning-Struct constructor trap

If a struct has private fields, external code can't use struct literal syntax. You must provide a public constructor function like Breakfast::summer() above.

Putting It All Together

Here's a realistic project structure:

my_server/
├── Cargo.toml
├── src/
│   ├── main.rs           # Binary: starts the server
│   ├── lib.rs            # Library: shared code
│   ├── config.rs         # Configuration module
│   ├── routes.rs         # Route definitions
│   └── handlers/
│       ├── mod.rs        # Handlers module (uses old style here)
│       ├── auth.rs       # Auth handlers
│       └── api.rs        # API handlers
└── tests/
    └── integration.rs    # Integration tests
// src/lib.rs
pub mod config;
pub mod routes;
pub mod handlers;

pub use config::Config;  // Re-export for convenience
// src/main.rs
use my_server::Config;

fn main() {
    let config = Config::from_env();
    // Start server...
}

The binary crate (main.rs) uses the library crate (lib.rs). This lets your integration tests also use the library, and lets others depend on your project as a library.

// Challenge

Quick Check: Bringing Items into Scope

What keyword brings an item into scope so you don't need the full path?

$

Summary

Rust's module system might feel strict at first, but it pays off. You get:

  • Clear boundaries: pub marks your public API explicitly
  • Flexibility: Reorganize internals without breaking external code
  • Discoverability: pub use creates clean, flat APIs

Start simple. One main.rs file works fine for small programs. As your code grows, split into modules. Extract a library crate when you want to share code or write integration tests.

// Tip-Start small

Don't over-organize early. Begin with one file, then extract modules when you notice logical groupings. Let the code tell you where the boundaries should be.

The key principles:

  1. Crates are the compilation unit (binary or library)
  2. Packages bundle crates with Cargo.toml
  3. Modules organize code within a crate
  4. Everything is private by default - add pub explicitly
  5. use brings paths into scope
  6. pub use re-exports for cleaner APIs

Next, we'll explore Rust's standard library collections: Vec, String, and HashMap. These dynamic data structures build on everything you've learned about ownership and borrowing.

<-[prev]Enums and Pattern Matching[next]->Collections
Progress: 0%
// On this page
  • Crates: The Building Block
  • Packages: Bundling Crates Together
  • Modules: Organizing Within a Crate
  • Nested Modules
  • Everything is Private by Default
  • Paths: Navigating Your Code
  • The use Keyword
  • Aliases with as
  • Re-exporting with pub use
  • Nested Paths
  • Structs vs Enums: A Privacy Difference
  • Putting It All Together
  • Summary