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.

Trending Tags