Single Project to Workspace

As you build projects in Rust, it's natural to start simple: a binary (src/main.rs) or a library (src/lib.rs) sitting neatly in a single Cargo project. But as your ideas grow, your project can benefit from a little more structure.

In this chapter, we'll walk through the journey of evolving a simple Rust project into a workspace. We'll see why you might want to do it, what changes are involved, and what benefits you gain along the way. If you're curious about the "next step" in organizing your Rust code, this is for you!

The Starting Point: One Project, One Crate

Let's imagine you begin with a simple library and binary combined in one project:

my-project/
|- Cargo.toml
|- src/
   |- lib.rs
   |- main.rs

The Cargo.toml declares both a library and a binary:

[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

[dependencies]

[lib]
path = "src/lib.rs"

[[bin]]
name = "my-project"
path = "src/main.rs"

You don't even need the [lib] and [[bin]] sections if you don't overwrite the default settings in the sample above as the name is identical to the project name.

This works beautifully for small programs. You can define reusable code in lib.rs, and build your CLI, server, or app in main.rs, calling into the library.

But what if you want to:

  • Add another binary (e.g., a CLI tool and a server)?
  • Create internal libraries that aren't part of the public API?

You surely can still put all logic into the lib crate but how about separating concerns more cleanly across crates? That's where workspaces shine!

Moving to a Workspace

A Cargo workspace is a way to manage multiple related crates together. Think of it like a "super-project" that coordinates building, testing, and managing dependencies across multiple packages.

Let's transform our project step-by-step.

Create a Cargo.toml for the workspace

Move the existing Cargo.toml into a new my-project/ sub-folder. Then, create a top-level Cargo.toml:

[workspace]
members = [
    "crates/my-project-lib",
    "crates/my-project-cli",
]

The members list tells Cargo which packages belong to the workspace.

Split the code into crates

We'll create two crates inside a new crates/ directory as implied in the above workspace Cargo.toml.

my-project/
|- Cargo.toml (workspace)
|- crates/
   |- my-project-lib/
      |- Cargo.toml
      |- src/
         |- lib.rs
   |- my-project-cli/
      |- Cargo.toml
      |- src/
         |- main.rs

my-project-lib will hold the reusable library code, while my-project-cli will be a binary crate depending on my-project-lib.

Update the crate dependencies

In crates/my-project-cli/Cargo.toml:

[package]
name = "my-project-cli"
version = "0.1.0"
edition = "2024"

[dependencies]
my-project-lib = { path = "../my-project-lib" }

Now your CLI crate can call into the library just like before. Spend some time to put the code and tests into the corresponding crate. It's a good brain excercise, with the help of cargo build --workspace, to define the clear crate bundary which might not be the case before.

Why Bother?

At first, moving to a workspace might feel like extra overhead. But it brings powerful benefits, even for relatively small projects:

  • Clear Separation of Concerns: Each crate focuses on a specific task. Your codebase becomes easier to understand and maintain.
  • Faster Builds: Cargo can rebuild only the crates that changed, rather than the entire project.
  • Multiple Binaries: You can easily add more binaries (tools, servers, utilities) alongside your main app.
  • Internal Libraries: Share code across multiple binaries without publishing it externally.
  • Testing in Isolation: You can run cargo test per-crate to get faster, more focused feedback.
  • Ready for Growth: When you eventually want to split parts into separate published crates (on crates.io) or keep internal libraries private, you're already halfway there.

In short, workspaces help your project scale without becoming messy.

A Natural Evolution

You don't have to start with a workspace when writing your first Rust project. But when your project grows just a little—adding a second binary, needing some internal shared code—workspaces offer a clean and powerful way to stay organized.

The best part? Moving to a workspace is an incremental change. You can migrate a project in stages, and Rust's tooling (Cargo) makes it smooth.

If you're curious, give it a try on your next project! You'll gain both clarity and flexibility. In this small CLI project of mine: jirun, I have transferred it into workspace style to prepare for growing :)!