DEV Community

Cover image for Rust: Packages, Modules, Crates...Oh My!
Jeff Mitchell
Jeff Mitchell

Posted on

Rust: Packages, Modules, Crates...Oh My!

Today, I will share my take on crates, packages, and modules in Rust. I've found Rust's organization system challenging to master, but with time and practice, it's finally starting to click.

Let's get the basics under our belt!

Crates

A crate is the smallest unit of a Rust program. For instance, the Rust compiler considers this code sample a crate:

fn main() {
    println!("I am a crate.");
}
Enter fullscreen mode Exit fullscreen mode

An important concept is the crate root. The crate root is where the compiler begins building a program. In the tiny example above, whatever we name the file, let's say something.rs, becomes the crate root.

There are two types of crates: binary crates and library crates.

  • A binary crate is self-contained, meaning it includes an executable file with a main function and can run independently.

  • A library crate, on the other hand, is a collection of functionality intended to be used by other crates. It doesn’t have a main function and can’t run on its own.

A common way to organize a Rust program is by splitting it into a binary crate and a library crate. The binary crate, often named main.rs, contains the executable file, while the library crate holds reusable functionality. The binary crate then imports types, methods, etc. from its library counterpart.

Packages

A package is a bundle of one or more crates that work together to provide functionality. Every package has a Cargo.toml file, which tells the Rust compiler how to build the included crates.

  • A package can contain multiple binary crates but only one library crate.
  • At a minimum, a package must include at least one crate (either binary or library).

The presence of a Cargo.toml file in the root project directory defines the package. By default, Cargo assumes src/main.rs is the root of a binary crate, and src/lib.rs is the root of a library crate. The package name defaults to the name of the binary or library crate, but you can customize this in Cargo.toml. For example:

[[bin]]
name = "fun-with-nom"
path = "src/bin/httpd.rs"

[lib]
name = "fun_with_nom_lib"
path = "src/lib/lib.rs"
Enter fullscreen mode Exit fullscreen mode

I’ve found it useful to organize my Rust projects with one binary crate for initialization and startup logic and a library crate for the program’s core functionality. For small projects, this structure might be overkill, but for larger codebases, such as APIs, it keeps the code modular and maintainable. Following this structure has been a lifesaver for “future me” when revisiting old projects.

Modules

Crates can be further divided into modules, which can live in a single file or across multiple files. Modules serve two main purposes:

  • Organization: Grouping related code into manageable units.
  • Privacy Control: Code in a module is private by default, meaning it isn’t visible outside the module unless explicitly made public.

While you can define all your modules in one file, this approach can quickly become unwieldy. Instead, it’s better to organize modules into separate files for easier navigation.

Paths

The Rust compiler uses paths to locate code. Paths are similar to file system paths on Windows, Linux, or macOS and can take two forms:

  • Absolute Paths: Start from the crate root. For external crates, this begins with the crate name. For code within the current crate, it begins with the literal crate.
  • Relative Paths: Start from the current module and use the keywords self, super, or an identifier in the current module. For example, super refers to the parent module.

The use Keyword

The use keyword brings a module into scope, allowing its contents to be accessed by other parts of the program. This is especially useful for avoiding repetitive path specifications.

For example:

use serde::Deserialize;
Enter fullscreen mode Exit fullscreen mode

Provided we've added the serde crate (including the derive feature flag) to the dependencies section of Cargo.toml, this line will bring the Deserialize macro into scope so that we can use it with our types.

The Namespace Operator

Rust’s namespace operator, ::, is used alongside the use keyword to access items within a module.

For example:

use axum::{http::StatusCode, routing::get, response::IntoResponse};
Enter fullscreen mode Exit fullscreen mode

Here, we bring the axum web application framework into scope and bring the following specific dependencies along for the ride:

  • the StatusCode type from the http module
  • the get method from the routing module the IntoResponse trait from the response module

If we're selecting certain things that we want to depend on, they're enclosed in {} braces.

Putting It All Together

To see these concepts in action, check out my repository Fun with Nom. It’s a project where I explored the Nom crate during a deep dive early in 2024.

Conclusion

At first, I struggled to understand how to effectively use crates, packages, and modules in Rust. But with practice, these concepts have become second nature. I hope this article helps anyone facing similar challenges.

Thanks for reading this introduction to organizing Rust programs. ! I'd love to start a discussion about your thoughts and experiences. Sound off in the comments!

References

Top comments (2)

Collapse
 
pgradot profile image
Pierre Gradot • Edited

Thanks for the explanation. I'm also getting started with Rust and organization and visibility is still not totally clear to me. There are other subtleties that you've mentioned, for instance cargo workspaces or integration tests made of several crates. I've got bitten with the former a few weeks ago, because you cannot access your library crate's private parts from integration tests (unlike unit tests, inside the library crate). I've still got some stuffs to process 😅

Collapse
 
crusty-rustacean profile image
Jeff Mitchell

My pleasure! I plan on writing about workspaces in the future, as they deserve treatment all on their own.