The Cargo Guide

Project Creation and Scaffolding

What Project Creation and Scaffolding Mean in Cargo

In Cargo, project creation is the process of creating a new Rust package with a valid Cargo.toml manifest and an initial source layout. Scaffolding is the starting structure Cargo creates so that the package is immediately buildable and follows standard Rust conventions.

The core tools are:

  • cargo new: create a new package in a new directory
  • cargo init: initialize a package inside an existing directory

These commands do more than make files. They establish a package identity, choose whether the package is a binary or library by default, optionally create version control metadata, and set up a layout that Cargo can understand immediately.

A useful mental model is:

  • cargo new is for starting a new project folder
  • cargo init is for turning an existing folder into a Cargo package

Starting a New Project with cargo new

The most common starting point is cargo new.

cargo new hello_app

This creates a new binary package named hello_app.

Typical result:

hello_app/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

Generated manifest:

[package]
name = "hello_app"
version = "0.1.0"
edition = "2024"
 
[dependencies]

Generated source:

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

You can build and run it immediately:

cd hello_app
cargo run

This matters pedagogically because Cargo does not leave the learner with an empty or ambiguous state. It creates a working Rust package from the start.

When to Use cargo init Instead

Use cargo init when you already have a directory and want to turn it into a Cargo package.

Example:

mkdir notes_tool
cd notes_tool
cargo init

That produces a package in the current directory rather than creating a new subdirectory.

Result:

notes_tool/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

This is especially useful when:

  • you already created the repository directory manually
  • you cloned an empty repository and want to add Cargo
  • you have an ad hoc Rust project and want to formalize it

A common distinction is:

cargo new my_app     # creates ./my_app/
cargo init           # initializes the current directory

Default Behavior: Binary Packages

By default, both cargo new and cargo init create a binary package unless told otherwise.

cargo new calculator

This creates src/main.rs, which means the package builds an executable.

fn main() {
    println!("Calculator starting...");
}

This default is a good match for beginners because executable programs are easy to run and inspect.

cargo run

The learner sees a quick feedback loop: create, run, modify, rerun.

Choosing --bin vs --lib

Cargo lets you choose whether the initial package is a binary or a library.

Explicit binary package:

cargo new cli_tool --bin

Explicit library package:

cargo new math_utils --lib

A library package uses src/lib.rs instead of src/main.rs.

Example layout:

math_utils/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── lib.rs

Generated library source might look like this:

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

The decision between --bin and --lib is really about intent:

  • choose --bin when the package is primarily a runnable program
  • choose --lib when the package is primarily reusable code

This is not permanent. A package can later grow to contain both a library and one or more binaries.

How to Think About Package Naming

Package naming matters early because Cargo uses the package name in several places.

Example:

cargo new weather_reporter

Manifest:

[package]
name = "weather_reporter"
version = "0.1.0"
edition = "2024"
 
[dependencies]

In Rust code, a package with a library target is typically referenced by its crate name. For example:

pub fn summarize() -> String {
    "Sunny".to_string()
}

Then elsewhere:

fn main() {
    println!("{}", weather_reporter::summarize());
}

Practical naming guidance:

  • use lowercase names
  • prefer clear, concise names
  • use underscores rather than spaces or punctuation
  • choose a name that still makes sense if the project grows

A poor name creates friction later in code, commands, repository structure, and publication.

Edition Selection

Cargo writes a Rust edition into Cargo.toml when it creates a package.

[package]
name = "hello_app"
version = "0.1.0"
edition = "2024"
 
[dependencies]

The edition controls certain language-level defaults and compatibility behavior. For a learner, the important point is that the edition is part of the package definition and should usually be left at the current stable default unless there is a specific reason to change it.

If needed, you can edit it manually:

[package]
name = "legacy_demo"
version = "0.1.0"
edition = "2021"
 
[dependencies]

A beginner-friendly rule is:

  • use the current edition for new learning projects
  • only change the edition deliberately, not casually

This keeps examples aligned with modern Rust style and tooling behavior.

VCS Initialization

Cargo can initialize version control metadata when creating a project. In many environments, cargo new will create a Git repository automatically if that behavior is enabled and suitable for the context.

You can also control this explicitly.

Create a project and force Git initialization:

cargo new inventory_app --vcs git

Disable version control initialization:

cargo new scratchpad --vcs none

This matters because scaffolding is not just about source files. For real development, project structure often includes source control from the beginning.

Example layout with Git initialized:

inventory_app/
ā”œā”€ā”€ .git/
ā”œā”€ā”€ .gitignore
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

For learning purposes, the main takeaway is that Cargo can scaffold both the Rust package and some of the surrounding project hygiene.

Initial Layout and Why It Matters

Cargo's initial layout is small on purpose.

Binary package:

my_app/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

Library package:

my_lib/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── lib.rs

This minimal structure teaches a strong lesson: Cargo relies on convention over configuration. The learner does not need to define every path manually.

For example, Cargo knows:

  • src/main.rs is the default binary target
  • src/lib.rs is the default library target

That lets a beginner focus on package intent and code, not on boilerplate configuration.

Creating a Package Inside an Existing Repository

A very common real-world case is an existing Git repository that does not yet contain a Cargo package.

Example:

mkdir task_tracker
cd task_tracker
git init
cargo init --bin

Result:

task_tracker/
ā”œā”€ā”€ .git/
ā”œā”€ā”€ .gitignore
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

This is often the right choice when:

  • the repository already exists on GitHub or another host
  • the project began as notes, scripts, or design docs
  • a team agreed on a repository first and Rust came later

cargo init is the bridge from "folder" to "Rust package."

Converting Ad Hoc Rust Code into a Cargo Package

Suppose you started with a standalone Rust file:

scratch/
└── hello.rs

Example file:

fn main() {
    println!("Hello from an ad hoc file");
}

You can compile it directly with rustc:

rustc hello.rs
./hello

But once the project grows, that workflow becomes limiting. To convert it into a Cargo package:

mkdir hello_project
mv hello.rs hello_project/
cd hello_project
cargo init --bin

Then replace the generated src/main.rs with your existing code:

fn main() {
    println!("Hello from an ad hoc file");
}

Or move the file into place:

mv hello.rs src/main.rs

Now the project has a proper manifest and can use Cargo workflows:

cargo run
cargo test
cargo build

This is an important transition because many learners start with loose files, but Cargo becomes more valuable as soon as the code needs dependencies, tests, or multiple targets.

Turning Existing Reusable Code into a Library Package

Sometimes the existing code is not really an application. It is reusable logic.

Suppose you have this file:

pub fn slugify(s: &str) -> String {
    s.to_lowercase().replace(' ', "-")
}

To turn that into a library package:

mkdir text_tools
cd text_tools
cargo init --lib

Then place the code in src/lib.rs:

pub fn slugify(s: &str) -> String {
    s.to_lowercase().replace(' ', "-")
}

You can test it with a unit test:

#[cfg(test)]
mod tests {
    use super::slugify;
 
    #[test]
    fn converts_spaces_to_hyphens() {
        assert_eq!(slugify("Hello World"), "hello-world");
    }
}

Then run:

cargo test

This reinforces the idea that scaffolding is not only about making empty projects; it is also about giving existing code the right package shape.

Growing from the Initial Scaffold to a Multi-Target Package

A package created with cargo new often starts with a single binary or library target, but it can grow naturally.

Start with:

cargo new data_tool
cd data_tool

Initial layout:

data_tool/
ā”œā”€ā”€ Cargo.toml
└── src/
    └── main.rs

Add a library target:

// src/lib.rs
pub fn normalize_name(name: &str) -> String {
    name.trim().to_lowercase()
}

Update the binary to use it:

// src/main.rs
fn main() {
    let value = data_tool::normalize_name("  Alice  ");
    println!("{value}");
}

Add another binary:

// src/bin/report.rs
fn main() {
    println!("{}", data_tool::normalize_name("  BOB  "));
}

Run the additional binary:

cargo run --bin report

The key insight is that the initial scaffold is not a cage. It is a starting point that can evolve into a more capable package structure.

A Practical Multi-Target Layout

Here is a meaningful package layout that grows from simple scaffolding into a more complete development shape:

reporting_tool/
ā”œā”€ā”€ Cargo.toml
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ lib.rs
│   ā”œā”€ā”€ main.rs
│   └── bin/
│       └── export.rs
ā”œā”€ā”€ examples/
│   └── quickstart.rs
└── tests/
    └── formatting.rs

Library logic:

// src/lib.rs
pub fn format_title(s: &str) -> String {
    s.trim().to_uppercase()
}

Main binary:

// src/main.rs
fn main() {
    println!("{}", reporting_tool::format_title(" monthly report "));
}

Additional binary:

// src/bin/export.rs
fn main() {
    println!("export: {}", reporting_tool::format_title(" annual summary "));
}

Example:

// examples/quickstart.rs
use reporting_tool::format_title;
 
fn main() {
    println!("{}", format_title(" draft report "));
}

Integration test:

// tests/formatting.rs
use reporting_tool::format_title;
 
#[test]
fn trims_and_uppercases() {
    assert_eq!(format_title(" draft report "), "DRAFT REPORT");
}

Useful commands:

cargo run
cargo run --bin export
cargo run --example quickstart
cargo test

This shows how a project can begin with standard scaffolding and grow into a package with reusable code, multiple executables, examples, and tests.

Common Mistakes During Project Creation

One common mistake is choosing --bin when the real goal is reusable code. Another is choosing --lib when the learner primarily wants a runnable CLI program.

A second mistake is creating Rust files manually without a manifest, then delaying the move to Cargo too long.

A third mistake is misunderstanding where files belong.

For example, this is correct:

src/main.rs
src/lib.rs
src/bin/helper.rs
examples/demo.rs
tests/integration_test.rs

But beginners sometimes create files in arbitrary places and then wonder why Cargo does not discover them.

The deeper lesson is that scaffolding works best when the project stays aligned with Cargo's conventions.

Hands-On Learning Sequence

A good progression for practice is to create several packages with different intentions.

First, create a binary package:

cargo new hello_cli --bin
cd hello_cli
cargo run

Then create a library package:

cd ..
cargo new string_tools --lib
cd string_tools
cargo test

Then initialize a package in an existing folder:

cd ..
mkdir notes_app
cd notes_app
cargo init --bin
cargo run

Then grow a package into a multi-target layout by adding:

  • src/lib.rs
  • src/bin/extra.rs
  • examples/demo.rs
  • tests/basic.rs

This sequence helps learners see that Cargo project creation is not one command to memorize, but a flexible scaffolding system for different project shapes.

Mental Model Summary

The main concepts fit together like this:

  • cargo new creates a new package in a new directory
  • cargo init creates a package in the current directory
  • --bin starts with an executable-oriented layout
  • --lib starts with a reusable-library layout
  • Cargo chooses a minimal initial scaffold based on convention
  • the package can later grow to include more targets
  • scaffolding is about giving code a standard, buildable structure from the beginning

A strong working model is:

"Cargo scaffolding creates the smallest standard Rust package that matches the kind of project I want to start, while leaving room for that package to grow."