---
title: How to Structure a Rust Project: Best Practices
summary: Structure a Rust project using Cargo to manage packages, crates, and modules for scalable code organization.
lang: en
slug: how-to-structure-a-rust-project-best-practices
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-structure-a-rust-project-best-practices/
---

# How to Structure a Rust Project: Best Practices



## The file that won't compile

You've written a script in Python that does exactly what you want. Now you're porting it to Rust. You start with `main.rs`. You add functions. The file grows to 400 lines. You decide to split the logic into `utils.rs`. You create the file. You add `mod utils;` to `main.rs`. The compiler rejects you. You add `pub` to the functions. It compiles. You celebrate.

Then you add a second binary. You try to share code between them. You copy-paste functions. You realize you're maintaining the same bug in two places. You look at your directory structure and feel like you're guessing. Is `lib.rs` only for libraries? Why does `cargo` care about `src/bin/`? How do modules map to files?

Rust's project structure feels rigid until you see the pattern. The rigidity is a feature. It forces you to separate the build configuration from the code, and the public API from the internal implementation. Once you internalize the rules, the structure stops fighting you and starts protecting you.

## Crate versus module

Rust distinguishes between a crate and a module. A crate is the unit of compilation. It's what `cargo` builds. A module is a namespace inside a crate. It's how you organize code.

Think of a crate as a shipping container. The container has a label, dimensions, and a manifest. That's `Cargo.toml`. Inside the container, you pack boxes. Those boxes are modules. You can arrange the boxes however you want, but the container itself has a fixed shape.

The file system defines the roots. `Cargo.toml` tells `cargo` where the crate lives. By default, `cargo` expects code in `src/`. If `src/main.rs` exists, `cargo` builds a binary crate. If `src/lib.rs` exists, `cargo` builds a library crate. If both exist, `cargo` builds both.

This dual-root behavior is the first surprise. A binary crate can have a library part. A library crate can have a binary part. The presence of `lib.rs` does not mean "this project is a library." It means "this crate exposes a library interface." You can use `lib.rs` to hold shared logic for multiple binaries in the same project.

## The minimal project

Start with `cargo new`. This command generates a manifest and a source directory. It sets up the defaults so you don't have to configure paths manually.

```bash
cargo new my_project
cd my_project
```

The output looks like this:

```text
my_project/
├── Cargo.toml
└── src/
    └── main.rs
```

`Cargo.toml` contains the package metadata and dependencies. `src/main.rs` contains the entry point.

```rust
// src/main.rs

/// Entry point for the binary.
fn main() {
    println!("Hello from my_project");
}
```

Run `cargo run`. The compiler builds the binary and executes it. The binary name matches the package name in `Cargo.toml`. If you rename the package, the binary name changes.

This structure works for simple tools. It breaks down when you need to share code or add entry points.

## Adding shared logic with lib.rs

You have a function that calculates a hash. You want to use it in `main.rs` and later in a test harness. Copying the function is a trap. You'll diverge. You'll fix a bug in one place and forget the other.

Create `src/lib.rs`. This file becomes the root of the library crate. You can declare modules here and expose them publicly.

```rust
// src/lib.rs

/// Utilities shared across the project.
pub mod utils;
```

```rust
// src/utils.rs

/// Computes a simple checksum for demonstration.
pub fn checksum(data: &[u8]) -> u64 {
    data.iter().map(|b| *b as u64).sum()
}
```

Now `main.rs` can use the function. You reference the library crate by the package name.

```rust
// src/main.rs

use my_project::utils::checksum;

/// Entry point for the binary.
fn main() {
    let data = b"hello";
    let sum = checksum(data);
    println!("Checksum: {}", sum);
}
```

The compiler resolves `my_project` to the library part of the crate. This works because `lib.rs` exists. The binary and library are two faces of the same crate. They share the same dependencies and the same version.

Convention aside: many beginners avoid `lib.rs` in binary projects because they think it's reserved for published libraries. That's a misconception. `lib.rs` is the home for shared logic. Use it early. It makes testing easier and keeps `main.rs` focused on orchestration.

Treat `lib.rs` as the heart of your crate. Even binaries beat better with one.

## Multiple binaries

Some projects produce more than one executable. A server might have a main daemon and a CLI tool for administration. A game might have an engine binary and a level editor.

Rust supports multiple binaries through `src/bin/`. Any `.rs` file in `src/bin/` becomes a separate binary. The binary name matches the file name.

```bash
mkdir -p src/bin
touch src/bin/admin.rs
```

```rust
// src/bin/admin.rs

use my_project::utils::checksum;

/// Admin tool for managing data.
fn main() {
    let data = b"admin-secret";
    println!("Admin checksum: {}", checksum(data));
}
```

Run `cargo run --bin admin`. `cargo` compiles the `admin` binary and runs it. You can also run the main binary with `cargo run`.

The `Cargo.toml` manifest lists the binaries automatically. `cargo` detects `src/main.rs` and files in `src/bin/`. You can override names or paths in `Cargo.toml` using `[[bin]]` sections, but the defaults usually suffice.

Convention aside: keep `src/main.rs` for the primary entry point. Use `src/bin/` for secondary tools. If your project has only one binary, stick with `src/main.rs`. Adding `src/bin/` just for one file adds noise.

## Modules and visibility

Modules organize code into namespaces. You declare modules with `mod`. You control access with `pub`.

The module tree starts at `lib.rs` or `main.rs`. From there, you branch out.

```rust
// src/lib.rs

pub mod utils;
pub mod config;
mod internal;
```

`pub mod` makes the module visible outside the crate. `mod` without `pub` keeps it private. The `internal` module can be used by `utils` and `config`, but external crates cannot access it.

Inside a module, you use `pub` on items to expose them.

```rust
// src/config.rs

/// Configuration settings.
pub struct Config {
    pub name: String,
    timeout: u64,
}

impl Config {
    /// Creates a new configuration with defaults.
    pub fn new(name: &str) -> Self {
        Self {
            name: name.to_string(),
            timeout: 30,
        }
    }
}
```

The `Config` struct is public. The `name` field is public. The `timeout` field is private. External code can read `name` but cannot modify `timeout` directly. This encapsulation prevents accidental misuse.

Convention aside: prefer `pub(crate)` over `pub` when you want to share code within the project but hide it from external users. `pub(crate)` restricts visibility to the current crate. It's the sweet spot for internal helpers that shouldn't leak into the public API.

```rust
// src/internal.rs

/// Helper used only within this crate.
pub(crate) fn parse_header(line: &str) -> Option<&str> {
    line.strip_prefix("HEADER:")
}
```

The compiler enforces these boundaries. If you try to access a private item from outside its module, you get an error.

```text
error[E0603]: module `internal` is private
```

This error code `E0603` means the module is not public. The fix is to add `pub` or `pub(crate)` where appropriate.

## Pitfalls and compiler errors

Structure mistakes show up as compiler errors. The errors are precise. They point to the missing declaration or the wrong visibility.

Forgetting `pub` on a struct is common. You define the struct in a module, use it in `main.rs`, and the compiler rejects you.

```text
error[E0603]: struct `Config` is private
```

The struct exists, but it's not exposed. Add `pub` to the struct definition.

Circular dependencies are impossible in Rust. The module tree is a directed acyclic graph. You cannot have `mod a` that depends on `mod b` which depends on `mod a`. The compiler catches this early.

File system mismatches cause `E0583`. You declare `mod foo;` but there's no `foo.rs` or `foo/mod.rs`.

```text
error[E0583]: file not found for module `foo`
```

Check the filename. Rust 2018 and later use `foo.rs` for modules. The old `mod.rs` style is deprecated. If you have a directory `foo/`, the module file is `foo/mod.rs` only if you want the module to be a directory. Otherwise, use `foo.rs`.

Convention aside: `cargo fmt` formats every file the same way. Don't argue style; argue logic. Run `cargo fmt` before committing. It removes noise and keeps the codebase consistent.

Trust the borrow checker. It usually has a point. Structure errors are often visibility errors. Fix the visibility, and the code compiles.

## Decision matrix

Use `cargo new` when you start a fresh project; it sets up the manifest and directory structure so you don't have to guess.

Use `src/lib.rs` when you have logic shared between multiple binaries or you want to expose a public API for other crates to depend on.

Use `src/bin/` when your project produces multiple executables, like a main server and a helper CLI tool.

Use `mod` declarations in `lib.rs` or `main.rs` to build a tree of modules that matches your logical separation, not just your file count.

Reach for `pub(crate)` when you want to share code within the project but hide it from external users.

Reach for `pub` only on the interface that external crates need to use. Keep implementation details private.

Start simple. Add complexity only when the compiler forces you to.

## Where to go next

- [How to Split a Crate into Multiple Files](/en/how-to-split-a-crate-into-multiple-files/)
- [How does the module system work](/en/how-does-the-module-system-work/)
- [How to Use Nested Paths (use std::{io, fs}) in Rust](/en/how-to-use-nested-paths-use-stdio-fs-in-rust/)