Monorepo with Rust: Organizing Code in Practice
\n\nWhen developing large projects in Rust, sooner or later the question arises: how to organize code so that it remains maintainable, reusable, and easily scalable? One popular approach is the monorepo. In this article, we will break down what a monorepo is in the context of Rust, how to set it up using Cargo Workspaces, and what practical techniques will help avoid chaos in the codebase.
The material will be useful both for beginners just starting to work with large projects and for experienced developers seeking effective ways to organize code.
\n\nWhat is a Monorepo and Why is it Needed in Rust?
\n\nA monorepo is an approach where all the code of a project (or several related projects) is stored in a single repository, as opposed to multi-repositories where each service or library lives in its own Git repository.
\n\nWhy do Rust developers often choose a monorepo?
\n- \n
- Single dependency version. You avoid conflicts when different crates require different versions of the same library. \n
- Simplified refactoring. Changing an API in one crate is immediately visible in all dependent crates — the Rust compiler will point out all the places that need fixing. \n
- Shared compilation. Cargo can optimize the build by reusing already compiled artifacts. \n
- Unified change history. You don't need to synchronize commits across different repositories. \n
The main tool for organizing a monorepo in Rust is Cargo Workspaces. Let's look at how to use it in practice.
\n\nSetting Up Cargo Workspaces: A Step-by-Step Example
\n\nSuppose we are creating an application consisting of three parts:
\n- \n
- the
core_liblibrary — common logic; \n - the
utilslibrary — utilities; \n - the
appexecutable — the main binary. \n
Let's create the monorepo structure:
\n\nmy_monorepo/\n├── Cargo.toml # root workspace manifest\n├── core_lib/\n│ ├── Cargo.toml\n│ └── src/\n│ └── lib.rs\n├── utils/\n│ ├── Cargo.toml\n│ └── src/\n│ └── lib.rs\n└── app/\n ├── Cargo.toml\n └── src/\n └── main.rs\n\n\nStep 1. Root Cargo.toml
[workspace]\nmembers = [\n "core_lib",\n "utils",\n "app",\n]\n\n\nNow all three crates are part of one workspace. Cargo will manage them as a single unit.
\n\nStep 2. Internal dependencies
\nIn the app/Cargo.toml file, we specify that it depends on our libraries:
[package]\nname = "app"\nversion = "0.1.0"\nedition = "2021"\n\n[dependencies]\ncore_lib = { path = "../core_lib" }\nutils = { path = "../utils" }\n\n\nStep 3. Writing code
\nExample code in core_lib/src/lib.rs:
pub fn greet(name: &str) -> String {\n format!("Hello, {}!", name)\n}\n\n\nIn app/src/main.rs we use this function:
use core_lib::greet;\n\nfn main() {\n println!("{}", greet("Rust Developer"));\n}\n\n\nStep 4. Building and running
\nThe commands cargo build, cargo test, and cargo run work for the entire workspace. You can also run a specific crate:
cargo run -p app\ncargo test -p core_lib\n\n\nThis is the basic setup. Now let's move on to more advanced practices.
\n\nAdvanced Organization: Modules, Tests, and Versioning
\n\nWhen there are more crates, it's important to maintain discipline. Here are a few