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"]
    end

Binary 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:

SpecificationMeaningAccepted 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 rangePer the range
"=1.2.3"Exact versionOnly 1.2.3
"*"All versionsThe 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 #

CrateFunctionCargo.toml
serde + serde_jsonJSON, YAML, TOML, etc. serialization/deserializationserde = { version = "1", features = ["derive"] }
tomlParse and write TOML filestoml = "0.8"
csvRead and write CSVcsv = "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 #

CrateFunctionCargo.toml
tokioThe most popular async runtimetokio = { version = "1", features = ["full"] }
reqwestHTTP client (sync and async)reqwest = { version = "0.11", features = ["json"] }
axumWeb framework built on tokioaxum = "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 #

CrateFunctionCargo.toml
anyhowEasy error handling for applicationsanyhow = "1"
thiserrorCustom error types with derivethiserror = "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 #

CrateFunctionCargo.toml
logLogging facade (abstract API)log = "0.4"
env_loggerLog implementation to stderrenv_logger = "0.10"
tracingStructured logging and tracingtracing = "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 #

CrateFunction
randRandom numbers
chronoDates and times
regexRegular expressions
uuidGenerate UUIDs
clapCLI argument parsing
rayonData parallelism
itertoolsExtra iterators
once_cellLazy statics (pre-OnceLock)

Summary #

  • Two kinds of crates — binary (has main(), produces an executable) and library (no main(), usable by other crates). One package can have both.
  • Cargo.toml is the crate contract — defines the name, version, dependencies, feature flags, and build profiles. Versions follow semantic versioning.
  • Don’t gitignore Cargo.lock for binaries — it guarantees reproducible builds. For libraries, Cargo.lock is usually gitignored so users get the latest compatible version.
  • The module system uses files and directoriesmod nama; in lib.rs looks for src/nama.rs or src/nama/mod.rs. All items are private by default; add pub to 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.lock and target directory. Avoid dependency duplication with workspace.dependencies.
  • serde is 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.
  • anyhow for applications, thiserror for libraries — both simplify error handling but in different ways: anyhow for “I don’t care about the exact error type”, thiserror for “I need structured error types for library users”.
  • Publishing can’t be undone — use cargo publish --dry-run before a real publish. Published versions can’t be deleted.

← Previous: Regex   Next: Multi Threading →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact