Polyglot
Languages Rust modules
Rust § modules

Cargo and packaging

Rust’s package and module system has two layers: Cargo (the package manager and build tool) and the module system (the in-source organisation). A crate is the smallest compilation unit — a single library or binary; a package is a Cargo project containing one or more crates with a Cargo.toml manifest. The module system organises items within a crate into a tree of modules (mod declarations), with visibility (pub) controlling what crosses module boundaries. The conventional Rust ecosystem revolves around crates.io (the public package registry) and Cargo’s substantial command surface (cargo new, cargo build, cargo test, cargo doc, cargo publish).

This page covers Cargo workflows, the module system, visibility, and the conventional patterns.

Cargo

Creating a project

cargo new myproject                              # binary
cargo new mylib --lib                            # library

A new project’s structure:

myproject/
├── Cargo.toml
├── Cargo.lock
└── src/
    └── main.rs              (or lib.rs for libraries)

Cargo.toml

The package manifest:

[package]
name = "myproject"
version = "0.1.0"
edition = "2024"
authors = ["Author <author@example.com>"]
description = "A project"
license = "MIT"
repository = "https://github.com/user/myproject"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
thiserror = "1"

[dev-dependencies]
criterion = "0.5"

[build-dependencies]
cc = "1"

[features]
default = []
async = ["tokio"]

The principal sections:

  • [package] — metadata.
  • [dependencies] — runtime dependencies.
  • [dev-dependencies] — only for cargo test and cargo bench.
  • [build-dependencies] — for build.rs scripts.
  • [features] — opt-in compilation features.

Dependency forms

[dependencies]
# From crates.io
serde = "1.0"
serde = { version = "1.0", features = ["derive"] }

# From git
my_lib = { git = "https://github.com/user/my_lib", branch = "main" }
my_lib = { git = "https://github.com/user/my_lib", rev = "abc123" }

# Local path
my_other = { path = "../my_other" }

# Optional (for features)
tokio = { version = "1", optional = true }

Cargo commands

cargo build                                      # debug build
cargo build --release                            # optimised build
cargo run                                        # build and execute
cargo run --release
cargo run -- arg1 arg2                           # pass args to the program

cargo check                                      # type-check without producing a binary
cargo test                                       # run tests
cargo test some_test                             # run only matching tests
cargo doc                                        # generate HTML documentation
cargo doc --open                                 # generate and open in browser

cargo new project                                # create new project
cargo init                                       # init in existing directory

cargo add serde                                  # add dependency
cargo add serde --features derive
cargo remove serde                               # remove dependency
cargo update                                     # update Cargo.lock

cargo fmt                                        # format the source
cargo clippy                                     # lint the source

cargo publish                                    # publish to crates.io
cargo install ripgrep                            # install a binary crate

Cargo workspaces

Multiple-crate projects (a workspace) share one Cargo.lock and target/:

# Cargo.toml at the workspace root:
[workspace]
members = [
    "core",
    "api",
    "cli",
]

[workspace.dependencies]
serde = "1.0"
# core/Cargo.toml:
[package]
name = "core"
version = "0.1.0"
edition = "2024"

[dependencies]
serde = { workspace = true }

The mechanism admits substantial reuse for monorepos and multi-crate libraries.

Modules

The module system organises code within a crate into a tree.

Inline modules

The mod keyword introduces a module:

mod math {
    pub fn add(a: i32, b: i32) -> i32 {
        a + b
    }

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

    pub mod advanced {
        pub fn power(base: i32, exp: u32) -> i32 {
            base.pow(exp)
        }
    }
}

fn main() {
    println!("{}", math::add(2, 3));
    println!("{}", math::advanced::power(2, 10));
}

The pub admits external visibility; without it, the item is module-private.

File-based modules

Modules conventionally live in their own files:

src/
├── main.rs              (declares: mod math;)
└── math.rs              (the math module)
// main.rs:
mod math;

fn main() {
    println!("{}", math::add(2, 3));
}
// math.rs:
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Submodules

Submodules can be inline or in a directory:

src/
├── main.rs              (mod math;)
└── math/
    ├── mod.rs           (mod basic; pub mod advanced;)
    ├── basic.rs
    └── advanced.rs

Or, the more contemporary form (Rust 2018+):

src/
├── main.rs              (mod math;)
├── math.rs              (mod basic; pub mod advanced;)
└── math/
    ├── basic.rs
    └── advanced.rs

The 2018+ form does not require mod.rs; math.rs declares the submodules in math/.

pub and visibility

The conventional visibility modifiers:

ModifierVisible to
(none)Same module
pubAnywhere
pub(crate)Same crate
pub(super)Parent module
pub(in path)Specified module
mod a {
    pub fn public() {}                          // visible everywhere
    pub(crate) fn crate_only() {}                // visible in the crate
    pub(super) fn parent_only() {}               // visible in the parent
    fn private() {}                              // only in module a

    pub mod b {
        pub fn nested() {
            super::private();                    // OK; same crate
        }
    }
}

The conventional discipline is to make items as private as possible; expose only the necessary surface.

use

The use keyword imports items into scope:

use std::collections::HashMap;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};

// Then:
let mut m: HashMap<String, i32> = HashMap::new();
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;

The conventional patterns:

use std::fmt;                                   // imports the module; fmt::Display
use std::fmt::Display;                          // imports the trait directly
use std::collections::*;                        // imports everything (rarely used)
use std::collections::{HashMap, HashSet};        // multiple items from one path
use std::io::{self, BufRead};                    // imports io and io::BufRead
use std::path::PathBuf as Path;                  // alias

The conventional discipline is to import the immediate parent (e.g., use std::fmt; and use fmt::Display); for traits commonly used as bare names (Read, Write, Iterator), import them directly.

Re-exports

The pub use admits exposing items at a different path:

mod implementation {
    pub fn do_thing() {}
}

pub use implementation::do_thing;               // exposes do_thing at the crate root

The mechanism admits a clean public API even when the implementation is split across modules.

// In a library's lib.rs:
mod parser;
mod lexer;
mod ast;

pub use parser::parse;                           // public API
pub use ast::{Expr, Stmt};

// User code:
use my_lib::{parse, Expr, Stmt};                  // simple paths

Crates and binaries

A crate is a compilation unit. The two principal kinds:

  • Library cratessrc/lib.rs is the root; produce a library.
  • Binary cratessrc/main.rs is the root; produce an executable.

A package may contain both:

myproject/
├── Cargo.toml
└── src/
    ├── lib.rs           (the library — reusable code)
    └── main.rs          (the binary — entry point)

Multiple binaries:

myproject/
├── Cargo.toml
└── src/
    ├── lib.rs
    └── bin/
        ├── tool1.rs
        └── tool2.rs

Each src/bin/*.rs is a separate binary; cargo run --bin tool1 runs the specific binary.

The standard prelude

The std::prelude::v1 is automatically imported into every module:

// Pre-imported (no use needed):
Option, Some, None
Result, Ok, Err
Box, String, Vec
Drop, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug
Iterator, IntoIterator, FromIterator, Extend, DoubleEndedIterator, ExactSizeIterator
ToString, AsRef, AsMut, From, Into, TryFrom, TryInto
println!, print!, eprintln!, eprint!, format!, write!, writeln!
vec!, panic!, assert!, assert_eq!, assert_ne!, debug_assert!, dbg!, todo!, unimplemented!, unreachable!

For everything else, explicit use is required.

Common patterns

Library structure

A small library’s typical layout:

mylib/
├── Cargo.toml
├── src/
│   ├── lib.rs           (public API; re-exports)
│   ├── parser.rs
│   ├── error.rs
│   └── types.rs
└── tests/
    └── integration.rs
// lib.rs:
mod parser;
mod error;
mod types;

pub use parser::parse;
pub use error::{Error, Result};
pub use types::{Token, Expr};

Binary with shared library

myproject/
├── Cargo.toml
└── src/
    ├── lib.rs           (reusable logic)
    └── main.rs          (CLI entry point)
// lib.rs:
pub fn process(input: &str) -> String {
    // ...
}
// main.rs:
use myproject::process;

fn main() {
    let input = std::env::args().nth(1).unwrap();
    println!("{}", process(&input));
}

Workspace

myproject/
├── Cargo.toml          (workspace root)
├── core/
│   ├── Cargo.toml
│   └── src/lib.rs
├── api/
│   ├── Cargo.toml      (depends on core)
│   └── src/lib.rs
└── cli/
    ├── Cargo.toml      (depends on api, core)
    └── src/main.rs

The mechanism admits substantial reuse and clean separation.

Tests

Unit tests within the same file:

// In src/math.rs:
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_adds() {
        assert_eq!(add(2, 3), 5);
    }
}

Integration tests in tests/:

// In tests/integration.rs:
use mylib::add;

#[test]
fn integration_test() {
    assert_eq!(add(2, 3), 5);
}

The cargo test runs both kinds.

Documentation tests

Code in doc-comments runs as tests:

/// Adds two numbers.
///
/// # Examples
///
/// ```
/// use mylib::add;
/// assert_eq!(add(2, 3), 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

The cargo test includes doc tests; cargo doc produces HTML documentation including the examples.

Features

Optional functionality controlled by feature flags:

[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]

[dependencies]
serde_json = { version = "1", optional = true }
serde_yaml = { version = "0.9", optional = true }
// In code:
#[cfg(feature = "json")]
mod json_support;

#[cfg(feature = "yaml")]
mod yaml_support;

The user enables features at build time:

cargo build --features yaml
cargo build --no-default-features --features yaml

Conditional compilation

The #[cfg] attribute admits platform- and configuration-specific code:

#[cfg(target_os = "linux")]
fn linux_specific() { /* ... */ }

#[cfg(target_os = "windows")]
fn windows_specific() { /* ... */ }

#[cfg(any(target_os = "linux", target_os = "macos"))]
fn unix_like() { /* ... */ }

#[cfg(test)]
mod tests { /* ... */ }                          // only compiled for tests

#[cfg(debug_assertions)]
fn debug_only() { /* ... */ }                    // only in debug builds

A note on crates.io

The public package registry. The conventional discipline:

  • Package names are kebab-casemy-lib, not my_lib.
  • Versions follow semver1.0.0, 0.1.0-alpha.1.
  • Documentation is hosted at docs.rs — automatically built and published.
  • Versioning discipline matters — pre-1.0 (0.x) admits breaking changes; post-1.0 requires major-version bumps for breaking changes.

Publishing:

cargo login                                      # authenticate
cargo publish                                    # publish current package

A note on the 2024 edition

The Rust 2024 edition (the conventional contemporary edition since late 2024) introduced:

  • Stabilised let-else.
  • Improved if-let chains.
  • Improved Cargo features.
  • Several minor breaking changes (admitted under edition gating).

The edition is declared in Cargo.toml; new projects from 2024 onwards default to edition = "2024". Crates of different editions interoperate.

A note on the conventional discipline

The contemporary Rust modules-and-packages advice:

  • One crate per package for libraries; admit binaries alongside if useful.
  • Use workspaces for multi-crate projects.
  • Keep lib.rs small — re-export the public API; defer details to submodules.
  • Use file-based modules (Rust 2018+ form, no mod.rs required).
  • Use pub(crate) for crate-internal helpers; pub for the external API.
  • Document with ///cargo doc produces excellent docs.
  • Test with #[cfg(test)] mod tests for unit tests; tests/ for integration.
  • Use cargo fmt and cargo clippy — the conventional formatting and linting.
  • Use cargo add and cargo remove (since 1.62) — admits managing dependencies without manual edits.

The combination — Cargo’s substantial workflow surface, the module system, crates.io, the standard prelude, edition-based versioning — is the substance of Rust’s package ecosystem. The mechanism admits substantial robustness, reuse, and tooling.