Introduction
As a Rust project grows, the question arises: how to organize code so that it remains maintainable, scalable, and convenient for the team? One popular approach is a monorepository (monorepo). In this article, we will break down what a monorepository is, how to organize it using Cargo workspace, and provide practical examples. You will learn how to structure code, manage dependencies, and avoid common mistakes.
What is a monorepository and why is it needed in Rust?
A monorepository (or monorepo) is a single repository that contains multiple projects (crates) or packages. Unlike multi-repositories, where each project lives separately, a monorepository combines everything: libraries, binaries, tests, configurations, and documentation.
Advantages of a monorepository for Rust:
- Single dependency version — all crates use the same versions of external packages, which reduces conflicts.
- Simplified refactoring — changes in one library are immediately visible in all dependent crates.
- End-to-end integration — you can easily run tests and linting for the entire project.
- Centralized documentation — all documents and CI/CD are in one place.
However, a monorepository is not suitable for everyone. For small projects or teams with different development cycles, it is better to choose a multi-repo. But for the Rust ecosystem, where modularity and code reuse are the norm, a monorepository becomes a powerful tool.
Organizing a monorepository with Cargo Workspace
Cargo supports the workspace mechanism — this is a group of crates that are built together and share a common Cargo.lock. Let's look at the basic structure.
Creating a workspace
Create a root directory and a Cargo.toml file without a [package] section, but with a [workspace] section:
# root Cargo.toml[workspace]members = [ "core", "cli", "web",]Now create the subdirectories core, cli, web. Each of them must have its own Cargo.toml with a typical package structure. For example, for core:
# core/Cargo.toml[package]name = "my_core"version = "0.1.0"edition = "2021"
[dependencies]serde = { version = "1.0", features = ["derive"] }For cli and web, you can specify a dependency on my_core:
# cli/Cargo.toml[package]name = "my_cli"version = "0.1.0"edition = "2021"
[dependencies]my_core = { path = "../core" }clap = { version = "4.0", features = ["derive"] }Now when you run cargo build from the root, all crates will be built, and Cargo.lock will be shared.
Folder and module structure
Recommended monorepository structure for Rust:
my_monorepo/├── Cargo.toml # workspace├── Cargo.lock├── core/ # library with common types│ ├── Cargo.toml│ └── src/│ └── lib.rs├── cli/ # console application│ ├── Cargo.toml│ └── src/│ └── main.rs├── web/ # web server (e.g., on Actix)│ ├── Cargo.toml│ └── src/│ └── main.rs├── tests/ # integration tests (optional)│ └── integration_test.rs├── scripts/ # scripts for CI/CD└── README.mdInside each crate, use the standard Rust module structure. For example, in core/src/lib.rs:
// core/src/lib.rspub mod models;pub mod utils;And in core/src/models.rs:
// core/src/models.rsuse serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]pub struct