How to Create a New Rust Project with Cargo

Use `cargo new` to initialize a new binary project or `cargo new --lib` for a library, then navigate into the directory to start coding.

When a file isn't enough

You just finished a script in Python that works, but it runs too slowly, or you want to ship a single binary to a friend without them installing a runtime. You open your terminal, ready to write code, but you realize you need a structure. In Rust, you don't just create a file and run it. You create a project.

Rust separates the language from the tooling. The compiler is rustc, but you almost never call it directly. Instead, you use Cargo. Cargo is the build system, package manager, and workflow enforcer rolled into one. It generates the directory structure, writes the configuration file, downloads dependencies, compiles the code, and runs tests. It removes the "works on my machine" problem by locking dependencies and ensuring every developer uses the exact same versions.

Think of Cargo as the skeleton of your project. You provide the muscle and brain; Cargo provides the bones, the joints, and the nervous system to connect everything. Without Cargo, you would manually manage include paths, compilation flags, and dependency downloads. With Cargo, you type a few commands and the ecosystem handles the rest.

The minimal start

Creating a project takes one command. Cargo generates the standard layout so you can focus on logic instead of scaffolding.

# Create a new binary project named "my_tool"
cargo new my_tool

# Enter the project directory
cd my_tool

# Build and run the project
cargo run

This sequence creates a directory called my_tool containing Cargo.toml and src/main.rs. The cargo run command compiles the code and executes the binary. You will see Hello, world! printed to the terminal.

The generated src/main.rs contains the entry point for your program.

/// The main entry point for the binary.
fn main() {
    // Print a greeting to stdout.
    println!("Hello, world!");
}

Cargo also creates a .gitignore file that excludes the target directory. This directory holds all build artifacts. You never commit target to version control. It can grow large and is fully reproducible from the source code.

Convention aside: Run cargo fmt immediately after creating a project. The Rust community uses a single formatter for all code. Running cargo fmt ensures your code matches the standard style from day one. You don't need to configure it; the defaults are the convention.

Anatomy of the manifest

The heart of every Cargo project is Cargo.toml. This file is written in TOML, a simple configuration format. It defines the package metadata and dependencies.

[package]
name = "my_tool"
version = "0.1.0"
edition = "2021"

[dependencies]

The [package] section contains metadata. The name must match the directory name. The version follows semantic versioning. The edition specifies which version of the Rust language to use. Edition 2021 is the current default. It includes features like async/await stabilization and improved error messages. Always set the edition explicitly. Relying on the default can cause confusion when the default changes in future Rust releases.

Convention aside: The edition field is not just a suggestion. It changes the language semantics. Code that compiles in edition 2018 might fail in edition 2021 if it relies on deprecated behavior. Pinning the edition ensures your project builds consistently across different toolchain versions.

Adding dependencies

Rust has a vast ecosystem of crates hosted on crates.io. You add them to your project using cargo add. This command updates Cargo.toml and downloads the crate.

# Add the serde crate with the derive feature
cargo add serde --features derive

This command modifies Cargo.toml to include the dependency.

[dependencies]
serde = { version = "1.0", features = ["derive"] }

The --features derive flag enables the derive feature of serde. This feature provides macros that automatically implement serialization traits. Without it, you would need to write manual implementations for every type.

After adding a dependency, Cargo creates a Cargo.lock file. This file records the exact versions of all dependencies, including transitive dependencies. It ensures that every build uses the same dependency tree.

Convention aside: Commit Cargo.lock for binary projects. This guarantees that your application builds with the exact same dependencies everywhere. Do not commit Cargo.lock for library crates. Library consumers need to resolve dependencies against their own dependency tree. If you commit the lock file for a library, you risk creating version conflicts for your users.

Libraries versus binaries

By default, cargo new creates a binary project. A binary produces an executable file. If you want to create a library that other projects can depend on, use the --lib flag.

# Create a library project
cargo new --lib math_utils

This generates src/lib.rs instead of src/main.rs. The lib.rs file contains the public interface of your library.

/// Add two integers together.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Functions in a library must be marked pub to be accessible to other crates. The /// doc comment generates documentation when you run cargo doc.

You can have both a library and a binary in the same project. Create a src/bin directory and add a file like src/bin/cli.rs. Cargo will treat this as a separate binary target. This pattern is useful when you want to test library code through a command-line interface.

The build workflow

Cargo provides commands for different stages of development. Choose the right command for your task.

# Check for errors without generating machine code
cargo check

# Build the project
cargo build

# Run the binary
cargo run

# Build with optimizations
cargo build --release

The cargo check command runs type checking and borrow checking without generating the final binary. It is significantly faster than cargo build. Use it for rapid feedback while you write code.

The cargo build command compiles the project and places the output in target/debug. This directory contains the binary and intermediate files. The debug build includes debug symbols and disables optimizations. It compiles quickly but runs slowly.

The cargo build --release command produces an optimized binary in target/release. This build enables optimizations like inlining and link-time optimization. It takes longer to compile but runs much faster. Always use the release build for performance testing or deployment.

Pitfall: If you run cargo run and see a permission error, check the target directory. Cargo creates target with specific permissions. If you manually modified files in target, you might have broken the permissions. Delete the target directory and run cargo clean to reset it.

Pitfalls and compiler errors

New Rust developers often encounter specific errors when setting up projects. Understanding these errors saves time.

If you add a dependency but forget to import it, the compiler rejects your code with E0432 (unresolved import). This error means you referenced a name that does not exist in the current scope. Check your use statements and ensure the dependency is listed in Cargo.toml.

If you use a type that does not implement a required trait, the compiler rejects your code with E0277 (trait bound not satisfied). This error often happens when you forget to derive a trait or enable a feature. For example, if you try to serialize a struct without #[derive(Serialize)], you will see this error.

If you try to move a value out of a borrowed reference, the compiler rejects your code with E0507 (cannot move out of borrowed content). This error indicates a violation of ownership rules. You need to clone the value or change the function signature to take ownership.

Convention aside: When you see E0277, check the feature flags. Many crates split functionality into features to reduce compile times. If a trait is missing, the crate documentation usually lists the feature required to enable it.

Decision matrix

Choose the right Cargo command based on your goal.

Use cargo new project_name when you are starting a fresh project and want Cargo to create the directory and files for you. Use cargo init when you already have a directory with files and want to turn it into a Rust project without moving anything. Use cargo check when you want fast feedback on syntax and type errors without the time cost of generating machine code. Use cargo build when you need the compiled binary or library file, such as for deployment or testing performance. Use cargo run when you are iterating on a binary and want to compile and execute in one step. Use cargo new --lib when you are building a library crate that other projects will depend on, rather than an executable program. Use cargo add when you want to add a dependency with the correct version syntax and feature flags. Use cargo fmt when you want to format your code according to community standards. Use cargo clippy when you want to catch common mistakes and improve code quality.

Trust Cargo. It knows where your binaries live. Don't fight the tooling; use the commands designed for each stage of development.

Where to go next