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:
| Concept | TypeScript / Node.js | Rust | Key file(s) |
|---|---|---|---|
| Package | NPM package | A project, described by Cargo.toml. Can contain one library crate and multiple binary crates. | Cargo.toml |
| Crate | Build entry / bundle | Compilation unit. The compiler compiles one crate at a time. Library or binary. | src/main.rs or src/lib.rs |
| Module | ES 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:
- Core library (
core) - Backend API (
server) - 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 hoistednode_modulesbut at build level.
6. External dependencies
Rust’s equivalent of npm install:
- Open
Cargo.toml. - Under
[dependencies]add:
1
rand = "0.8.5"
- In code:
1
use rand::Rng; // rand comes from Cargo.toml
Std vs external:
std::...: Standard library (like JS built-ins; in Rust you usuallyusethem).rand::...: External crate (like something fromnode_modules).
Summary: TS vs Rust modules
| Aspect | TypeScript | Rust | Idea |
|---|---|---|---|
| Import | import { x } from './file' | mod file; use file::x; | TS: file-based; Rust: tree-based |
| Project root | package.json | Cargo.toml | Deps and build config |
| Namespace | namespace (rare) / file | mod | Logical boundaries |
| Default visibility | File scope / export for public | Private | Rust: closed by default |
| Crate-internal | No (comment or don’t export) | pub(crate) | Visible inside crate only |
| Monorepo | Pnpm/Yarn workspaces | Cargo workspaces | Shared deps and build |
| Entry | index.ts | lib.rs / main.rs | Root 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.