Crates #
A crate is the smallest compilation and distribution unit in Rust — the equivalent of a package in npm, a gem in Ruby, or a module in Go. All Rust code lives inside a crate, and Cargo is the tool that manages crates: downloading dependencies, compiling, running tests, and publishing. Understanding the crate and Cargo system isn’t just about being able to add a dependency to Cargo.toml — there are features like feature flags, workspaces, and path dependencies that significantly affect how you structure a project. This article covers everything from the foundations to the patterns used in real production projects, closing with a list of essential crates that appear in almost every Rust project.
The Two Kinds of Crates #
Rust recognizes two kinds of crates that produce different outputs:
flowchart LR
subgraph Binary Crate
B["src/main.rs\n(crate root)"] --> BE["Binary/Executable\n.exe / extensionless file"]
end
subgraph Library Crate
L["src/lib.rs\n(crate root)"] --> LE["Library\n.rlib / .so / .dll"]
end
subgraph Both
BL["src/main.rs\n+\nsrc/lib.rs"] --> BLE["Binary that\nuses its own library"]
endBinary crates produce a runnable program. They must have a main() function. There can be several binaries in one package (in src/bin/).
Library crates produce code that other crates can use. They have no main(). A package can only have one library crate (src/lib.rs).
A package can contain both — a library and one or more binaries that use that library:
proyek-ku/
├── Cargo.toml
└── src/
├── lib.rs ← library crate (main logic)
├── main.rs ← main binary (uses lib.rs)
└── bin/
├── server.rs ← additional binary
└── migrate.rs ← additional binary
Project Structure and Cargo.toml
#
Cargo.toml is the heart of every crate — it defines metadata, dependencies, and build configuration:
[package]
name = "aplikasi-ku" # Crate name (must be lowercase, can use -)
version = "0.1.0" # Semantic versioning: MAJOR.MINOR.PATCH
edition = "2021" # Rust edition: "2015", "2018", or "2021"
authors = ["Budi <[email protected]>"]
description = "Example Rust application"
license = "MIT OR Apache-2.0"
repository = "https://github.com/budi/aplikasi-ku"
homepage = "https://aplikasi-ku.com"
keywords = ["cli", "utility"]
categories = ["command-line-utilities"]
readme = "README.md"
[dependencies]
# Version from crates.io
serde = "1.0"
# Version with additional features
serde = { version = "1.0", features = ["derive"] }
# Specific version
regex = "1.10.3"
# Version range
tokio = ">=1.0, <2.0"
[dev-dependencies]
# Only used while testing, not bundled into the binary
tempfile = "3"
mockall = "0.11"
[build-dependencies]
# Used by build.rs (build script)
cc = "1"
[features]
# Conditional features — discussed in more detail later
default = ["json"]
json = ["serde/derive", "serde_json"]
async = ["tokio"]
full = ["json", "async"]
[[bin]]
# Additional binaries besides main.rs
name = "server"
path = "src/bin/server.rs"
[profile.release]
# Optimizations for release builds
opt-level = 3
lto = true # Link-time optimization
codegen-units = 1 # Slower compile, smaller and faster binary
strip = true # Remove debug symbols
Managing Dependency Versions #
Cargo uses Semantic Versioning. How you write the version in Cargo.toml determines which versions are considered compatible:
| Specification | Meaning | Accepted versions |
|---|---|---|
"1.2.3" | Compatible with 1.2.3 | >=1.2.3, <2.0.0 |
"^1.2.3" | Same as above | >=1.2.3, <2.0.0 |
"~1.2.3" | Patch compatible | >=1.2.3, <1.3.0 |
"1.2.*" | Minor compatible | >=1.2.0, <1.3.0 |
">=1.2, <2" | Explicit range | Per the range |
"=1.2.3" | Exact version | Only 1.2.3 |
"*" | All versions | The latest version |
Frequently Used Cargo Commands #
# Creating a new project
cargo new nama-proyek # binary (default)
cargo new nama-lib --lib # library
# Build and run
cargo build # debug build
cargo build --release # release build (slower, more optimized)
cargo run # build + run
cargo run --bin nama-binary # run a specific binary
cargo run -- --argumen # pass arguments to the program
# Testing
cargo test # all tests
cargo test nama_fungsi # filter tests by name
cargo test --doc # only doc tests
cargo test --release # test with release optimizations
# Dependencies
cargo add serde # add a dependency (cargo-add, Rust 1.62+)
cargo add serde --features derive
cargo remove serde # remove a dependency
cargo update # update dependencies to the latest compatible version
cargo tree # show the dependency tree
# Documentation
cargo doc # generate documentation
cargo doc --open # generate + open in the browser
# Linting and formatting
cargo clippy # run the linter (clippy must be installed)
cargo fmt # format code (rustfmt must be installed)
cargo check # check errors without building (faster than cargo build)
# Publishing
cargo login # log in to crates.io with an API token
cargo publish --dry-run # simulate publishing without actually publishing
cargo publish # publish to crates.io
The Module System #
A crate can be organized into nested modules. Modules control namespacing and visibility:
// src/lib.rs — the crate root for a library
// Inline module
pub mod matematika {
pub fn tambah(a: f64, b: f64) -> f64 {
a + b
}
// Nested module
pub mod trigonometri {
pub fn sin(x: f64) -> f64 {
x.sin()
}
pub fn cos(x: f64) -> f64 {
x.cos()
}
}
// Private item — only available inside this module
fn helper_internal() -> &'static str {
"not exposed outside"
}
}
// Module from a separate file — Rust looks for src/geometri.rs or src/geometri/mod.rs
pub mod geometri;
// Module from a file in a subdirectory (Rust 2018+)
// Rust looks for src/io/file.rs
pub mod io {
pub mod file;
}
// src/geometri.rs
pub struct Persegi {
pub sisi: f64,
}
impl Persegi {
pub fn luas(&self) -> f64 {
self.sisi * self.sisi
}
}
// Non-pub items can only be accessed from inside the geometri module
fn helper() {}
Visibility #
pub mod kontrol {
// pub — public to everyone
pub fn publik() {}
// without pub — private, only within this module
fn privat() {}
// pub(crate) — public within the crate, private outside
pub(crate) fn publik_dalam_crate() {}
// pub(super) — public to the parent module
pub(super) fn publik_ke_parent() {}
pub mod sub {
// pub(in path) — public to a specific path
pub(in crate::kontrol) fn publik_ke_kontrol() {}
}
}
use for Shortening Paths
#
// Without use — the full path every time
fn contoh() {
let map = std::collections::HashMap::<&str, i32>::new();
}
// With use — more concise
use std::collections::HashMap;
use std::io::{self, Read, Write}; // multiple imports
fn contoh() {
let map = HashMap::<&str, i32>::new();
}
// Aliasing with as
use std::collections::HashMap as Map;
use std::fmt::Display as Tampilkan;
// Re-export — expose items from another module as if this crate owned them
pub use crate::geometri::Persegi; // crate users can access it directly
Feature Flags — Conditional Dependencies #
Feature flags let you enable or disable parts of a crate based on the build configuration:
# Cargo.toml
[features]
default = ["json"] # features active by default
json = ["serde_json"] # enable the serde_json dependency
async = ["tokio/full"] # enable the "full" feature of the tokio crate
tls = ["rustls"]
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true } # only if the "json" feature is active
tokio = { version = "1", optional = true }
rustls = { version = "0.21", optional = true }
// src/lib.rs — conditional code based on features
// Only compiled if the "json" feature is active
#[cfg(feature = "json")]
pub mod json {
use serde_json::Value;
pub fn parse(s: &str) -> Result<Value, serde_json::Error> {
serde_json::from_str(s)
}
}
// Only compiled if the "async" feature is active
#[cfg(feature = "async")]
pub async fn fetch(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let resp = reqwest::get(url).await?.text().await?;
Ok(resp)
}
// Code that differs by platform
#[cfg(target_os = "linux")]
fn implementasi_linux() { /* ... */ }
#[cfg(target_os = "windows")]
fn implementasi_windows() { /* ... */ }
#[cfg(debug_assertions)]
fn hanya_di_debug() {
println!("[DEBUG] Internal data: ...");
}
Enabling features at build time:
cargo build --features "json,async"
cargo build --all-features # enable all features
cargo build --no-default-features # disable default features
cargo build --no-default-features --features "tls"
Workspaces — Multi-Crate Projects #
A workspace lets several crates live in one repository while sharing a single Cargo.lock and target directory:
proyek-besar/
├── Cargo.toml ← workspace root
├── Cargo.lock ← shared lock file
├── target/ ← shared build output
├── crates/
│ ├── core/ ← main library crate
│ │ ├── Cargo.toml
│ │ └── src/lib.rs
│ ├── api/ ← API server
│ │ ├── Cargo.toml
│ │ └── src/main.rs
│ └── cli/ ← CLI tool
│ ├── Cargo.toml
│ └── src/main.rs
└── tools/
└── codegen/
├── Cargo.toml
└── src/main.rs
# Cargo.toml at the workspace root
[workspace]
members = [
"crates/core",
"crates/api",
"crates/cli",
"tools/codegen",
]
# Shared dependencies that members can inherit
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# crates/api/Cargo.toml
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
core = { path = "../core" } # use a local crate
serde.workspace = true # inherit from the workspace
tokio.workspace = true # inherit from the workspace
# Cargo commands for workspaces
cargo build # build all members
cargo build -p api # build only the "api" crate
cargo test --workspace # test all members
cargo run -p cli -- --help # run a binary from the "cli" crate
Creating and Publishing a Crate #
The steps to publish a crate to crates.io:
# 1. Make sure Cargo.toml is complete
# (name, version, description, license, repository are required)
# 2. Write good documentation (/// on every public item)
cargo doc --open # preview the documentation
# 3. Run all tests
cargo test --all-features
# 4. Check for warnings
cargo clippy
# 5. Log in to crates.io
cargo login [API_TOKEN]
# The API token can be obtained from https://crates.io/me
# 6. Dry run — simulate without actually publishing
cargo publish --dry-run
# 7. Publish!
cargo publish
# Minimal Cargo.toml for publishing
[package]
name = "nama-crate-ku"
version = "0.1.0"
edition = "2021"
description = "A short description of this crate"
license = "MIT"
repository = "https://github.com/user/nama-crate-ku"
Once a crate is published to crates.io, that version cannot be deleted or changed — it can only be yanked (marked as don’t-use, but still downloadable). Make sure the code is correct before publishing. Use --dry-run to verify.Essential Crates Worth Knowing #
Here are the crates that appear most often in production Rust projects, grouped by category:
Serialization and Data #
| Crate | Function | Cargo.toml |
|---|---|---|
serde + serde_json | JSON, YAML, TOML, etc. serialization/deserialization | serde = { version = "1", features = ["derive"] } |
toml | Parse and write TOML files | toml = "0.8" |
csv | Read and write CSV | csv = "1" |
// serde — serialization/deserialization
use serde::{Deserialize, Serialize};
use serde_json;
#[derive(Debug, Serialize, Deserialize)]
struct Pengguna {
nama: String,
usia: u8,
aktif: bool,
}
fn main() {
let user = Pengguna { nama: "Budi".into(), usia: 28, aktif: true };
// Struct → JSON string
let json = serde_json::to_string_pretty(&user).unwrap();
println!("{}", json);
// JSON string → Struct
let kembali: Pengguna = serde_json::from_str(&json).unwrap();
println!("{:?}", kembali);
}
Async Runtime and Networking #
| Crate | Function | Cargo.toml |
|---|---|---|
tokio | The most popular async runtime | tokio = { version = "1", features = ["full"] } |
reqwest | HTTP client (sync and async) | reqwest = { version = "0.11", features = ["json"] } |
axum | Web framework built on tokio | axum = "0.7" |
// tokio + reqwest — asynchronous HTTP requests
use reqwest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let resp = reqwest::get("https://httpbin.org/ip")
.await?
.json::<serde_json::Value>()
.await?;
println!("Your IP: {}", resp["origin"]);
Ok(())
}
Error Handling #
| Crate | Function | Cargo.toml |
|---|---|---|
anyhow | Easy error handling for applications | anyhow = "1" |
thiserror | Custom error types with derive | thiserror = "1" |
// anyhow — very concise for applications
use anyhow::{Context, Result};
fn baca_konfigurasi(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file '{}'", path))
}
fn main() -> Result<()> {
let config = baca_konfigurasi("config.toml")?;
println!("{}", config);
Ok(())
}
// thiserror — expressive custom error types for libraries
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("File not found: {0}")]
FileNotFound(String),
#[error("Parse failed: {0}")]
ParseError(#[from] std::num::ParseIntError),
#[error("Connection failed after {detik} seconds")]
ConnectionTimeout { detik: u64 },
}
Logging #
| Crate | Function | Cargo.toml |
|---|---|---|
log | Logging facade (abstract API) | log = "0.4" |
env_logger | Log implementation to stderr | env_logger = "0.10" |
tracing | Structured logging and tracing | tracing = "0.1" |
// log + env_logger
use log::{debug, error, info, warn};
fn main() {
env_logger::init(); // reads the RUST_LOG env var
info!("Application started");
debug!("Debug detail: value={}", 42);
warn!("Warning: memory almost full");
error!("Critical error: {}", "connection failed");
}
// Run with: RUST_LOG=debug cargo run
Common Utilities #
| Crate | Function |
|---|---|
rand | Random numbers |
chrono | Dates and times |
regex | Regular expressions |
uuid | Generate UUIDs |
clap | CLI argument parsing |
rayon | Data parallelism |
itertools | Extra iterators |
once_cell | Lazy statics (pre-OnceLock) |
Summary #
- Two kinds of crates — binary (has
main(), produces an executable) and library (nomain(), usable by other crates). One package can have both.Cargo.tomlis the crate contract — defines the name, version, dependencies, feature flags, and build profiles. Versions follow semantic versioning.- Don’t gitignore
Cargo.lockfor binaries — it guarantees reproducible builds. For libraries,Cargo.lockis usually gitignored so users get the latest compatible version.- The module system uses files and directories —
mod nama;inlib.rslooks forsrc/nama.rsorsrc/nama/mod.rs. All items are private by default; addpubto expose them.- Feature flags for optional dependencies —
#[cfg(feature = "nama")]compiles code only when the feature is active. Useful for making a crate modular without forcing dependencies not everyone needs.- Workspaces for large projects — several crates in one repository share a single
Cargo.lockandtargetdirectory. Avoid dependency duplication withworkspace.dependencies.serdeis a crate that’s almost always present — serialization/deserialization to JSON, YAML, TOML, and other formats.#[derive(Serialize, Deserialize)]saves dozens of lines of code.anyhowfor applications,thiserrorfor libraries — both simplify error handling but in different ways:anyhowfor “I don’t care about the exact error type”,thiserrorfor “I need structured error types for library users”.- Publishing can’t be undone — use
cargo publish --dry-runbefore a real publish. Published versions can’t be deleted.