Introduction: Why Do You Need a Monorepo in Rust?
When you start learning Rust, the first thing you encounter is a single project created via cargo new. But what do you do when your project grows: shared libraries appear, multiple executable files (e.g., a server and a client), or you want to extract repetitive code into a separate package?
This is where the monorepo approach comes to the rescue — it is an approach where the code of several interconnected projects (libraries and executables) is stored in a single repository. Rust provides a built-in solution for this — Cargo Workspaces.
In this article, we will break down how to organize a monorepo in Rust from scratch: starting from the folder structure to practical examples. You will learn how to properly split code into modules, connect local dependencies, and manage the build.
1. What is a Cargo Workspace and How to Create It?
A workspace is a set of crates that share the same Cargo.lock file and a common target directory. This helps avoid dependency version conflicts and speeds up compilation.
Creating the Monorepo Structure
Let's create a simple monorepo with two crates: a library utils and an executable app.
mkdir my_monorepocd my_monorepocargo new utils --libcargo new appNow, let's create the root Cargo.toml file that declares the workspace:
[workspace]members = [ "utils", "app",]Important: Make sure that in the utils/Cargo.toml and app/Cargo.toml files there is no [package] section with a workspace field — it is not needed. Cargo will understand that this is part of the workspace.
Project Structure
After creation, you will have the following structure:
my_monorepo/├── Cargo.toml # root workspace├── utils/│ ├── Cargo.toml│ └── src/│ └── lib.rs└── app/ ├── Cargo.toml └── src/ └── main.rs2. Code Organization: Libraries and Modules
Now let's add code to the utils library. Suppose we want to create common functions for working with numbers and strings.
Library Code (utils/src/lib.rs)
pub mod math;pub mod strings;Let's create the module files:
// utils/src/math.rspub fn add(a: i32, b: i32) -> i32 { a + b}
pub fn multiply(a: i32, b: i32) -> i32 { a * b}// utils/src/strings.rspub fn greet(name: &str) -> String { format!("Hello, {}!", name)}
pub fn to_uppercase(text: &str) -> String { text.to_uppercase()}Connecting the Library in the Executable File
In the app/Cargo.toml file, specify the dependency on the local crate:
[package]name = "app"version = "0.1.0"edition = "2021"
[dependencies]utils = { path = "../utils" }Now in app/src/main.rs we can use functions from utils:
use utils::math;use utils::strings;
fn main() { let sum = math::add(5, 10); println!("5 + 10 = {}", sum);
let greeting = strings::greet("World"); println!("{}", greeting);
let upper = strings::to_uppercase("rust rocks"); println!("{}", upper);}Run the project from the root of the monorepo with the command:
cargo run -p appYou will see the output:
5 + 10 = 15Hello, World!RUST ROCKS3. Advanced Organization: Multiple Executable Files
Often in a monorepo, you need to have multiple