Thanks to visit codestin.com
Credit goes to jevy.wang

Post

15. Packages and Crates

15. Packages and Crates

If you’re used to the NPM ecosystem and ES modules, you might think “module” = file and “package” = folder in node_modules. In Rust, the same ideas exist for compile performance and strict boundaries.

TypeScript’s module system is file-system based (if the file exists, you can import it). Rust’s is explicit mounting (the file must be declared as part of the module tree).


Organizing code: from NPM to Cargo

1. Package vs Crate vs Module

In TS, “project,” “package,” and “module” are often mixed. In Rust the hierarchy is strict:

ConceptTypeScript / Node.jsRustKey file(s)
PackageNPM packageA project, described by Cargo.toml. Can contain one library crate and multiple binary crates.Cargo.toml
CrateBuild entry / bundleCompilation unit. The compiler compiles one crate at a time. Library or binary.src/main.rs or src/lib.rs
ModuleES module (a .ts file)Unit of organization; scoping and privacy.mod keyword

1.1 What is a Crate?

In TS you run tsc and compile the whole project. In Rust, rustc compiles one crate at a time.

  • Binary crate: Produces an executable (src/main.rs).
  • Library crate: Produces a library for other crates (src/lib.rs).

A package has one Cargo.toml and can have:

  • At most one library crate.
  • Any number of binary crates (e.g. under src/bin/).

2. Modules: the counter-intuitive part

This is where TS developers hit the wall.

2.1 Explicit declaration

In TS you add utils.ts and then import { func } from './utils'.

In Rust, if you add src/utils.rs, the compiler doesn’t see it until you declare it. You must declare it in a parent (usually main.rs or lib.rs):

1
2
3
4
5
6
7
8
// src/main.rs

mod utils;  // "Load utils.rs (or utils/mod.rs) and mount it as a child module named utils"

fn main() {
    utils::do_something();
}

Mental model: Modules form a logical tree. The file system is just one way to store that tree. You have to “mount” files onto the tree with mod.

2.2 Two ways to define a module

Suppose we want crate::front_of_house::hosting.

Option A: Inline (like a TS namespace) — for small modules.

1
2
3
4
5
6
7
// src/lib.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

Option B: File system — more like TS.

1
2
3
src/
├── lib.rs
└── front_of_house.rs   (or front_of_house/mod.rs)
1
2
3
4
5
6
7
8
9
// src/lib.rs
mod front_of_house;  // Content lives in another file

pub use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

Note: Since Rust 2018 you don’t have to use front_of_house/mod.rs (like index.js); you can use front_of_house.rs. If you have submodules, you still use a directory and either front_of_house.rs + front_of_house/... or front_of_house/mod.rs.


3. Paths and use

3.1 Absolute vs relative paths

  • TS: Absolute: import ... from '@/utils' (tsconfig paths); Relative: import ... from '../../utils'.
  • Rust:
    • crate:: (absolute): From the crate root (main.rs / lib.rs). Like TS @/.
    • super:: (relative): Parent module. Like ../.
    • self:: (relative): Current module. Like ./.
1
2
crate::front_of_house::hosting::add_to_waitlist();
super::hosting::add_to_waitlist();

3.2 The use keyword

use is like TS import or a short alias: it brings a path into scope.

1
2
3
4
5
use crate::front_of_house::hosting::add_to_waitlist;

pub fn eat_at_restaurant() {
    add_to_waitlist();
}

3.3 Re-exporting (pub use) — facade pattern

In TS we often do export * from './internal-module' in index.ts to shape the public API.

In Rust that’s pub use (re-export). It decouples internal layout from public API.

1
2
3
4
// src/lib.rs
mod front_of_house;  // Internal, can be private

pub use crate::front_of_house::hosting;  // Public API: users do use my_crate::hosting;

4. Visibility: privacy by default

TS has public (default) and private (compile-time only). In Rust, visibility is strictly enforced.

4.1 Default: private

Everything (functions, structs, modules, fields) is private by default. A parent module cannot see a child’s private items; children can see the parent’s (and ancestor’s) items (visibility is lexical).

4.2 pub

  • TS: export const foo = ...
  • Rust: pub fn foo() ...

4.3 Fine-grained visibility (no TS equivalent)

  • pub: Visible everywhere.
  • pub(crate): Visible only inside the current crate. Like “package private” in other languages.
  • pub(super): Visible only to the parent module.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mod back_of_house {
    pub struct Breakfast {
        pub toast: String,      // Customer can choose
        seasonal_fruit: String,  // Private; chef decides
    }

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

pub fn eat_at_restaurant() {
    let mut meal = back_of_house::Breakfast::summer("Rye");
    meal.toast = String::from("Wheat"); // ✅ Public field
    // meal.seasonal_fruit = ...;       // ❌ Private
}

5. Workspaces: monorepo support

You may have used Lerna, Nx, Turborepo, or Pnpm Workspaces. Cargo has workspaces built in.

Example: Project my-app with:

  1. Core library (core)
  2. Backend API (server)
  3. CLI (cli)

Layout:

1
2
3
4
5
6
7
8
9
my-app/
├── Cargo.toml   (workspace root)
├── crates/
│   ├── core/
│   │   └── Cargo.toml
│   ├── server/
│   │   └── Cargo.toml
│   └── cli/
│       └── Cargo.toml

Root Cargo.toml:

1
2
3
4
5
6
[workspace]
members = [
    "crates/core",
    "crates/server",
    "crates/cli",
]

In crates/server/Cargo.toml:

1
2
[dependencies]
core = { path = "../core" }

Benefits:

  • Shared Cargo.lock: Same dependency versions across crates.
  • Shared target: Common dependencies (e.g. serde) compile once; all crates reuse. Similar to hoisted node_modules but at build level.

6. External dependencies

Rust’s equivalent of npm install:

  1. Open Cargo.toml.
  2. Under [dependencies] add:
1
rand = "0.8.5"
  1. In code:
1
use rand::Rng;  // rand comes from Cargo.toml

Std vs external:

  • std::...: Standard library (like JS built-ins; in Rust you usually use them).
  • rand::...: External crate (like something from node_modules).

Summary: TS vs Rust modules

AspectTypeScriptRustIdea
Importimport { x } from './file'mod file; use file::x;TS: file-based; Rust: tree-based
Project rootpackage.jsonCargo.tomlDeps and build config
Namespacenamespace (rare) / filemodLogical boundaries
Default visibilityFile scope / export for publicPrivateRust: closed by default
Crate-internalNo (comment or don’t export)pub(crate)Visible inside crate only
MonorepoPnpm/Yarn workspacesCargo workspacesShared deps and build
Entryindex.tslib.rs / main.rsRoot of the module tree

Takeaway: The explicit mod declarations in Rust aren’t redundant; they give the compiler a fixed dependency graph without scanning imports. That’s part of why Rust can do aggressive dead code elimination and linking.

Practical tip: Before writing code, sketch the module tree: Crate root -> mod A -> mod B -> struct MyData. Then wire it with mod and use.

This post is licensed under CC BY 4.0 by the author.