Rust CratesModulesPackagesRustVisibility

Article & Tutorial

Rust Modules, Packages, Crates and Workspaces: Complete Project Structure Guide

Learn how Rust projects are organized with modules, packages, crates, visibility, use paths and Cargo workspaces, with practical project layouts and examples.

Rust Modules, Packages, Crates and Workspaces: Complete Project Structure Guide article illustration
Rust Modules, Packages, Crates and Workspaces: Complete Project Structure Guide article illustration
Contents

Rust Learning Path — Chapter 16. Once a Rust program grows beyond a few files, good project structure becomes essential. Rust gives you several related building blocks—modules, crates, packages, and workspaces—that help you organize code without sacrificing compile-time safety.

This chapter explains how those pieces fit together, how visibility works with pub, how use paths simplify imports, how to split code across files, and when a Cargo workspace is the right choice.

1. The terminology first

ConceptMeaning
ModuleA namespace inside a crate used to organize items.
CrateThe smallest compilation unit in Rust; either a binary crate or library crate.
PackageA Cargo project described by one Cargo.toml ; it contains one or more crate targets.
WorkspaceA group of packages that share a Cargo lockfile and target directory.

2. Binary and library crates

A project created with cargo new my_app normally starts as a binary crate because Cargo creates src/main.rs.

bash
my_app/ ├── Cargo.toml └── src/ └── main.rs

A library project can be created with:

bash
cargo new my_library --lib

That creates src/lib.rs. A package may contain both library and binary targets, which is common for real applications: shared business logic stays in the library while the executable only handles startup and wiring.

3. Create modules with mod

Suppose an application has configuration and API logic:

bash
// src/main.rs mod config; mod api; fn main() { let cfg = config::load(); api::start(cfg); }

Then create src/config.rs:

bash
pub struct Config { pub port: u16, } pub fn load() -> Config { Config { port: 8080 } }

And src/api.rs:

bash
use crate::config::Config; pub fn start(config: Config) { println!("API listening on {}", config.port); }

4. Visibility and pub

Rust items are private to their parent module by default. Use pub only where another module really needs access.

bash
pub struct User { pub id: u64, email: String, }

Here User and id are public, while email remains private. This lets a module protect its own invariants instead of exposing every field.

5. Absolute and relative module paths

You can refer to an item using a path from the crate root:

bash
use crate::config::Config;

Or with a relative path such as super:: when moving to a parent module:

bash
use super::shared::RequestId;

For application code, clear absolute paths from crate:: are often easier to understand as the codebase grows.

6. Nested modules and directories

A larger API module might look like:

bash
src/ ├── main.rs ├── api.rs └── api/ ├── users.rs └── health.rs

src/api.rs declares the submodules:

bash
pub mod health; pub mod users;

This is a good pattern when one logical module needs several source files.

7. Re-export a clean public API

Re-exporting lets callers use a shorter, more stable path:

bash
// src/lib.rs mod config; pub use config::Config;

Consumers can now write:

bash
use my_library::Config;

instead of depending on the internal module layout.

8. Multiple binaries

A package can expose several executables under src/bin:

bash
src/ ├── lib.rs └── bin/ ├── api.rs └── worker.rs

Run them with:

bash
cargo run --bin api cargo run --bin worker

This is useful when an application has an HTTP server and a background worker sharing the same library.

9. Cargo workspaces

When several related packages should be developed together, use a workspace:

bash
platform/ ├── Cargo.toml ├── api/ │ └── Cargo.toml ├── core/ │ └── Cargo.toml └── worker/ └── Cargo.toml

Workspace root:

bash
[workspace] resolver = "2" members = ["api", "core", "worker"]

Now Cargo can build or test all members together:

bash
cargo build --workspace cargo test --workspace

10. Workspace dependencies

Shared dependency versions can be declared centrally:

bash
[workspace.dependencies] serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] }

Then a member package can inherit them:

bash
[dependencies] serde.workspace = true tokio.workspace = true

11. Suggested backend layout

bash
src/ ├── main.rs ├── app.rs ├── config.rs ├── error.rs ├── routes/ │ ├── mod.rs │ ├── health.rs │ └── users.rs ├── services/ │ ├── mod.rs │ └── user_service.rs └── repositories/ ├── mod.rs └── user_repository.rs

The exact folder names matter less than keeping responsibilities clear and dependencies flowing in sensible directions.

12. Common mistakes

  • Making everything pub just to silence visibility errors.
  • Creating dozens of tiny modules before the code actually needs them.
  • Putting all logic in main.rs .
  • Using a workspace for a project that only needs one package.
  • Exposing internal modules instead of re-exporting a small public API.

13. Mini lab: split an application

Create a project and refactor it into three modules:

bash
cargo new rust_structure_lab cd rust_structure_lab

Create config, service, and output modules. Keep fields private where possible, expose only necessary functions, and verify with:

bash
cargo fmt cargo clippy cargo test cargo run

Key takeaways

  • Modules organize names inside a crate.
  • Crates are compilation units.
  • A Cargo package contains crate targets.
  • Workspaces group related packages and share build artifacts.
  • Keep APIs intentionally small with privacy and re-exports.

Did this help?

Yes No

Join the discussion

0 Comments

Ask questions, add context, or share what worked.

Sign in to join the discussion

No comments yet. Start the discussion.

More from Melochip Editorial

Related articles

Stay in the loop. Get new Melochip articles

Receive Melochip article updates when new articles are available. Explicit opt-in only.

No pre-checked boxes. Unsubscribe support is part of the subscriber foundation.