| |

Rust 3 🦀 Hello World and Project Structure in Cargo

Every programming language has its ritual first program, and Rust’s is no exception. But where most languages treat “Hello, world!” as a trivial one-liner, Rust’s version introduces you to the project structure that will define every non-trivial program you write. The cargo new command does not just create a file with a print statement — it scaffolds a complete project with a manifest, a source directory, a git repository, and a build configuration that you will use for the rest of your Rust career.

Understanding this structure is more important than the hello world itself. The layout of a Cargo project determines how modules resolve, where tests live, how binaries and libraries coexist, and how the compiler finds your code. Beginners who skip this knowledge often struggle later when their projects grow and they need to add a second binary, split code into modules, or organize tests. Learning the conventions now saves hours of confusion later.

This chapter walks through creating a project, examining every generated file, understanding the crate root concept, and exploring how Cargo’s conventions scale from a single main.rs to multi-binary, multi-library projects. We will also cover the cargo run, cargo check, and cargo clean commands that round out the basic development loop.

Key point: A Cargo project is more than a folder with a source file — it is a structured crate with a manifest, conventional source locations, and a build system that understands how to find, compile, and link every part of your code.


Why the Cargo project structure exists

The convention over configuration problem. Early build systems required developers to specify every source file, every include path, and every compilation step. This gave maximum flexibility but produced build files that were often longer and more complex than the code they built. Cargo takes the opposite approach: if you follow the conventions — src/main.rs for binaries, src/lib.rs for libraries, tests/ for integration tests — Cargo knows what to do without any configuration at all. The Cargo.toml manifest only needs to describe metadata and dependencies, not build steps.

The crate concept problem. In C and C++, the compiler works on translation units — individual .c or .cpp files — and the programmer must manage include paths and linking manually. Rust introduces the crate as the fundamental compilation unit. A crate is a tree of modules rooted at a single file: either main.rs for a binary or lib.rs for a library. This single-entry-point model simplifies compilation enormously because the compiler always knows where to start reading.

The multi-target problem. Real projects often produce more than one artifact. A tool might ship both a command-line binary and a library that other projects can depend on. Another project might produce several binaries that share code. Cargo handles this through directory conventions: src/main.rs and src/lib.rs can coexist, src/bin/ can hold additional binaries, and examples/ can hold example programs. Each target has a conventional location, and Cargo discovers them automatically.

The reproducibility problem. When you clone a Rust project, you expect to build it with a single command and no manual setup. The Cargo structure makes this possible because everything needed to build — the manifest, the source files, the lock file — lives in predictable places. There is no autotools configure step, no CMake generation phase, no Makefile to read. cargo build works because the conventions were followed.

The tooling integration problem. Tools like cargo test, cargo doc, and cargo fmt need to know where tests, documentation comments, and source files live. By standardizing the project layout, Cargo lets these tools work without per-project configuration. cargo test finds unit tests inside src/ files and integration tests inside tests/ automatically. cargo doc generates documentation for every public item in every crate in the dependency graph.


a. Creating the project and running hello world

The first step in any Rust project is cargo new. This command creates a directory, initializes a git repository, writes a manifest, and generates a starter source file. Run it with a project name:

cargo new hello_world
cd hello_world

Cargo prints a confirmation line, and you now have a working project. The name you pass to cargo new becomes the package name in Cargo.toml and the default binary name. Package names must be valid Rust identifiers — no spaces, no hyphens if you plan to use the crate as a library, though hyphens are allowed for binary-only packages.

To run the generated program, use:

cargo run

Cargo compiles the project and executes the resulting binary. On the first run, it also creates a target/ directory to hold build artifacts and a Cargo.lock file to record dependency versions. The output appears at the end of the compilation messages.

If you want to compile without running, use cargo build. To check the code for errors without producing a binary, use cargo check, which is significantly faster because it skips code generation. These three commands — run, build, and check — form the core of the development loop.

b. The generated files and their purpose

Running cargo new hello_world produces four items: a Cargo.toml file, a src/ directory containing main.rs, a .gitignore file, and a git repository in the directory itself. Each serves a specific role.

The Cargo.toml manifest is the project’s identity card. For a fresh binary project it looks like this:

[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"

[dependencies]

The [package] table holds metadata. name is the crate name, used when publishing and when other crates depend on yours. version follows semantic versioning and starts at 0.1.0. edition selects the language edition — Rust’s mechanism for introducing backwards-incompatible changes without breaking existing code. The current edition is 2024; earlier editions include 2015, 2018, and 2021. The [dependencies] table starts empty and grows as you add external crates.

The src/main.rs file is the crate root for the binary. Its contents are the canonical hello world:

fn main() {
    println!("Hello, world!");
}

The main function is the entry point for any Rust binary. The println! macro prints to standard output with a trailing newline. The ! distinguishes macros from functions — a syntactic marker that expands to code at compile time.

The .gitignore file excludes build artifacts from version control:

/target

The target/ directory can grow to hundreds of megabytes and is entirely regenerable. Excluding it keeps repositories small and avoids platform-specific binary conflicts. Cargo also creates a Cargo.lock file on first build; for binaries it should be committed, and Cargo’s generated .gitignore does not exclude it.

c. The crate root and module resolution

Every Rust crate has exactly one crate root. For a binary it is src/main.rs; for a library it is src/lib.rs. The compiler starts here and follows mod declarations to build the full module tree. This is why you pass Cargo the project directory rather than a list of files — it locates the crate root by convention and compiles everything reachable from it.

Adding a module begins with a mod declaration in the crate root:

mod greeting;

fn main() {
    greeting::say_hello();
}

The compiler then looks for the module’s code in one of two places: src/greeting.rs or src/greeting/mod.rs. The file-based form is preferred for simple modules, while the directory-based form accommodates modules with submodules. A module declared as mod greeting; in main.rs expects either src/greeting.rs or src/greeting/mod.rs.

When projects grow, Cargo supports multiple binaries through src/bin/. Each file in that directory becomes its own binary with a name matching the filename:

// src/bin/goodbye.rs
fn main() {
    println!("Goodbye, world!");
}

Running cargo run --bin goodbye executes this binary while cargo run continues to run the default binary from main.rs. Binaries in src/bin/ share dependencies declared in Cargo.toml but do not share code unless that code lives in a library.


Complete Example Session

# ============================================
# PART 1: CREATE THE PROJECT
# ============================================
# cargo new scaffolds a complete project.

cargo new hello_world
cd hello_world
# ============================================
# PART 2: EXAMINE CARGO.TOML
# ============================================
# The manifest describes the package.

[package]
name = "hello_world"
version = "0.1.0"
edition = "2024"

[dependencies]
// ============================================
// PART 3: EXAMINE MAIN.RS
// ============================================
// The crate root for the binary.

fn main() {
    println!("Hello, world!");
}
# ============================================
# PART 4: BUILD AND RUN
# ============================================
# Compile and execute in one step.

cargo run

# Output:
#    Compiling hello_world v0.1.0
#     Finished dev [unoptimized + debuginfo] target(s)
#      Running `target/debug/hello_world`
# Hello, world!
# ============================================
# PART 5: CHECK WITHOUT BUILDING
# ============================================
# cargo check validates code faster than build.

cargo check

# Output:
#    Checking hello_world v0.1.0
#     Finished dev [unoptimized + debuginfo] target(s)
// ============================================
// PART 6: ADD A MODULE
// ============================================
// Create src/greeting.rs and declare it.

// src/greeting.rs:
pub fn say_hello() {
    println!("Hello from a module!");
}
// ============================================
// PART 7: USE THE MODULE FROM MAIN
// ============================================
// The crate root declares the module.

// src/main.rs:
mod greeting;

fn main() {
    println!("Hello, world!");
    greeting::say_hello();
}
// ============================================
// PART 8: ADD A SECOND BINARY
// ============================================
// Files in src/bin/ become separate binaries.

// src/bin/goodbye.rs:
fn main() {
    println!("Goodbye, world!");
}
# ============================================
# PART 9: RUN A SPECIFIC BINARY
# ============================================
# Use --bin to select which binary to run.

cargo run --bin goodbye

# Output:
#      Running `target/debug/goodbye`
# Goodbye, world!
# ============================================
# PART 10: CLEAN BUILD ARTIFACTS
# ============================================
# Remove the target directory when needed.

cargo clean

# Output:
# Removed 42 files, 1.2MiB total

These ten parts cover the lifecycle of a Cargo project: creation, inspection, running, checking, adding modules, adding binaries, selecting binaries, and cleaning. Each step builds on the conventions Cargo establishes, and each is a command you will use repeatedly in real projects.


Quick Reference

Basic Cargo Commands

CommandPurpose
cargo new <name>Create binary project
cargo new --lib <name>Create library project
cargo initInitialize project in existing directory
cargo runBuild and run default binary
cargo run --bin <name>Run a specific binary
cargo buildCompile in debug mode
cargo checkType-check without code generation
cargo cleanRemove the target directory

Conventional Source Locations

PathPurpose
src/main.rsDefault binary crate root
src/lib.rsLibrary crate root
src/bin/*.rsAdditional binaries
src/<name>.rsModule file
src/<name>/mod.rsModule with submodules
tests/*.rsIntegration tests
examples/*.rsExample programs
benches/*.rsBenchmarks

Manifest Sections

SectionPurpose
[package]Name, version, edition, metadata
[dependencies]Runtime dependencies
[dev-dependencies]Test and example dependencies
[build-dependencies]Build script dependencies
[features]Optional features and their dependencies
[[bin]]Custom binary target configuration
[lib]Custom library target configuration

Best Practices

✅ Do This:

mod utils;                                   // Declare module in crate root
pub fn helper() {}                           // Export public items with pub
cargo run --bin my_tool                      // Select binaries explicitly
cargo check                                  // Use during development for speed
cargo new my_project                         // Use hyphens only for binary-only crates
cargo new my_library --lib                   // Use underscores for library names

❌ Don’t Do This:

mod utils;                                   // ❌ Declared but never used; warning
fn helper() {}                               // ❌ Private; inaccessible outside module
cargo run                                    // ❌ Ambiguous when multiple binaries exist
cargo build                                  // ❌ Slower than check during iteration
cargo new my-project                         // ❌ Hyphens cause issues for libraries
println!("Hello")                            // ❌ Missing semicolon and newline

Common Pitfalls

PitfallWhy It HappensFix
Module not foundFile does not exist at expected pathCheck src/<name>.rs or src/<name>/mod.rs
main not foundBinary file missing fn main()Add a main function to every binary
Multiple binaries, ambiguous runcargo run with several binaries presentUse cargo run --bin <name>
Cargo.lock not createdNo dependencies yetRun cargo build once; lock file appears
Changes not reflectedStale build artifactsRun cargo clean then rebuild
Wrong edition behaviorManifest says edition = "2015"Set edition = "2024" explicitly

Real-World Examples

1. Initialize in an Existing Directory

cd existing_folder
cargo init
# Adds Cargo.toml and src/main.rs without creating a new folder

2. Library with a Binary

// src/lib.rs — library crate root
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

3. Binary That Uses the Library

// src/main.rs — uses the library by crate name
fn main() {
    println!("{}", hello_world::greet("Rust"));
}

4. Multiple Binaries Sharing a Library

src/
├── lib.rs
├── main.rs
└── bin/
    ├── tool_a.rs
    └── tool_b.rs

5. Module with Submodules

// src/network/mod.rs
pub mod tcp;
pub mod udp;

6. Integration Test

// tests/integration.rs
#[test]
fn it_works() {
    assert_eq!(hello_world::greet("test"), "Hello, test!");
}

7. Example Program

// examples/demo.rs
fn main() {
    println!("{}", hello_world::greet("example"));
}

8. Custom Binary Target

[[bin]]
name = "custom_name"
path = "src/custom_entry.rs"

9. Workspace with Multiple Crates

[workspace]
members = ["crate_a", "crate_b"]

10. Build Script

// build.rs
fn main() {
    println!("cargo:rerun-if-changed=build.rs");
}

Visual

Generated Project Layout

┌──────────────────────────────────────────────────────────────┐
│  AFTER cargo new hello_world                                 │
│                                                              │
│  hello_world/                                                │
│  │                                                           │
│  ├── Cargo.toml          ← Manifest (name, version, edition) │
│  ├── .gitignore          ← Excludes /target                  │
│  │                                                           │
│  ├── .git/               ← Initialized git repository        │
│  │                                                           │
│  └── src/                                                    │
│      └── main.rs         ← Crate root for the binary         │
│                                                              │
│  After first build:                                          │
│  ├── Cargo.lock          ← Exact dependency versions         │
│  └── target/             ← Build artifacts                   │
│      └── debug/                                              │
│          └── hello_world ← Executable                        │
└──────────────────────────────────────────────────────────────┘

Module Resolution from Crate Root

┌──────────────────────────────────────────────────────────────┐
│  HOW mod DECLARATIONS RESOLVE                                │
│                                                              │
│  src/main.rs:                                                │
│  ┌────────────────────────────────────────────────────────┐  │
│  │ mod greeting;                                          │  │
│  │ mod network;                                           │  │
│  │ fn main() { greeting::say_hello(); }                   │  │
│  └────────────────────────────────────────────────────────┘  │
│         │                    │                               │
│         ▼                    ▼                               │
│  ┌──────────────┐    ┌─────────────────────┐                 │
│  │ src/         │    │ src/                │                 │
│  │ greeting.rs  │    │ network/            │                 │
│  │              │    │ └── mod.rs          │                 │
│  │              │    │     ├── tcp.rs      │                 │
│  │              │    │     └── udp.rs      │                 │
│  └──────────────┘    └─────────────────────┘                 │
│                                                              │
│  Either form works. Directory with mod.rs (or newer          │
│  network.rs + network/ combo) supports submodules.           │
└──────────────────────────────────────────────────────────────┘

Binary and Library Coexistence

┌──────────────────────────────────────────────────────────────┐
│  src/ WITH BOTH main.rs AND lib.rs                           │
│                                                              │
│  src/                                                        │
│  ├── lib.rs         ← Library crate root                     │
│  │   pub fn greet() → reusable by other crates               │
│  │                                                           │
│  ├── main.rs        ← Binary crate root                      │
│  │   fn main() → calls hello_world::greet()                  │
│  │                                                           │
│  └── bin/                                                    │
│      ├── tool_a.rs  ← Separate binary                        │
│      └── tool_b.rs  ← Separate binary                        │
│                                                              │
│  Cargo builds:                                               │
│  1. libhello_world.rlib from lib.rs                          │
│  2. hello_world from main.rs (links the lib)                 │
│  3. tool_a and tool_b (each links the lib)                   │
│                                                              │
│  All binaries share the library's code.                      │
└──────────────────────────────────────────────────────────────┘

Development Loop Commands

┌──────────────────────────────────────────────────────────────┐
│  THE EDIT-BUILD-RUN CYCLE                                    │
│                                                              │
│         ┌──────────────┐                                     │
│         │  Edit code   │                                     │
│         └──────┬───────┘                                     │
│                │                                             │
│                ▼                                             │
│         ┌──────────────┐                                     │
│         │ cargo check  │  ← Fastest feedback                 │
│         │              │    Type errors only                 │
│         └──────┬───────┘                                     │
│                │                                             │
│                ▼                                             │
│         ┌──────────────┐                                     │
│         │  cargo run   │  ← Full build and execute           │
│         │              │                                     │
│         └──────┬───────┘                                     │
│                │                                             │
│                ▼                                             │
│         ┌──────────────┐                                     │
│         │ Test output  │                                     │
│         └──────┬───────┘                                     │
│                │                                             │
│                └──────────▶ back to edit                     │
│                                                              │
│  cargo check is 2-5x faster than cargo build because         │
│  it skips code generation. Use it constantly.                │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Project creationcargo new <name> or cargo init for existing directories
ManifestCargo.toml — metadata and dependencies
Lock fileCargo.lock — exact versions, created on first build
Binary crate rootsrc/main.rs containing fn main()
Library crate rootsrc/lib.rs
Additional binariessrc/bin/*.rs, each with its own main
Module filesrc/<name>.rs or src/<name>/mod.rs
Integration teststests/*.rs
Examplesexamples/*.rs
Build artifactstarget/ directory, excluded from git
Default commandcargo run builds and executes
Fast checkcargo check validates without codegen

Key takeaways:

  • Cargo new scaffolds a complete project. One command produces a manifest, source directory, git repo, and gitignore file ready for development.
  • Conventions replace configuration. Cargo finds binaries, libraries, tests, and examples by location, so Cargo.toml only needs metadata and dependencies.
  • Every crate has one root. src/main.rs for binaries, src/lib.rs for libraries; the compiler follows mod declarations from there.
  • Modules resolve from the crate root. A mod greeting; declaration expects src/greeting.rs or src/greeting/mod.rs.
  • Multiple binaries are conventional. Files in src/bin/ each become a separate binary, selectable with cargo run --bin <name>.
  • Library and binary can coexist. A project with both lib.rs and main.rs builds a library and a binary that links to it.
  • cargo check speeds up iteration. It validates types without generating code, typically two to five times faster than a full build.
  • Cargo.lock belongs in version control for binaries. For libraries, exclude it so downstream users can select compatible versions.

Remember: The Cargo project structure is a set of conventions that eliminate configuration. By placing files in expected locations — src/main.rs, src/lib.rs, src/bin/, tests/ — you tell Cargo everything it needs to know without writing build rules. The crate root is the entry point from which the compiler discovers all reachable code through mod declarations. Learn this layout early, and every Rust project you encounter will feel familiar. The hello world program itself is trivial, but the structure around it is the foundation on which every larger Rust project is built.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!