Introdução
Quando seu projeto Rust cresce além de um único crate, a organização do código se torna um desafio real. Você começa com um binário simples, depois extrai uma biblioteca, adiciona uma CLI, talvez um servidor web, e logo tem 5+ crates que precisam compartilhar dependências e compilar juntos. É aqui que os Cargo workspaces brilham.
Um workspace é um conjunto de crates gerenciados por um único Cargo.lock, com compilação unificada e dependências compartilhadas. Grandes projetos Rust como o Tokio, Axum e o próprio compilador do Rust usam workspaces extensivamente.
Neste guia, vamos montar um workspace do zero, explorar os recursos avançados do Cargo para workspaces e compartilhar boas práticas que aprendemos com projetos reais.
Por que Usar Workspaces?
Sem workspaces, cada crate tem seu próprio Cargo.lock e compila suas dependências independentemente. Isso causa:
- Duplicação de compilação: o mesmo crate compilado N vezes
- Versões divergentes: um crate usa
serde 1.0.200, outro usaserde 1.0.198 - Builds lentos: sem cache compartilhado entre crates
- Gerenciamento manual: atualizar dependências em vários
Cargo.toml
Com workspaces, você ganha:
- Um único
Cargo.lockpara todo o projeto - Cache de compilação compartilhado no mesmo diretório
target/ - Dependências unificadas com
workspace.dependencies - Comandos como
cargo test --workspaceque rodam tudo de uma vez
Criando um Workspace do Zero
Vamos criar um projeto com 3 crates: uma biblioteca core, uma CLI e um servidor HTTP.
1. Estrutura Inicial
mkdir meu-projeto && cd meu-projeto
Crie o Cargo.toml raiz. Ele não define um [package] — apenas o workspace:
[workspace]
members = [
"core",
"cli",
"servidor",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Sua Equipe <equipe@exemplo.com>"]
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"
clap = { version = "4", features = ["derive"] }
axum = "0.8"
2. Crate Core (Biblioteca)
cargo init core --lib
Edite core/Cargo.toml:
[package]
name = "meu-projeto-core"
version.workspace = true
edition.workspace = true
[dependencies]
serde = { workspace = true }
serde_json = { workspace = true }
anyhow = { workspace = true }
Crie core/src/lib.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Usuario {
pub id: u64,
pub nome: String,
pub email: String,
}
impl Usuario {
pub fn novo(id: u64, nome: impl Into<String>, email: impl Into<String>) -> Self {
Self {
id,
nome: nome.into(),
email: email.into(),
}
}
pub fn validar(&self) -> anyhow::Result<()> {
if self.nome.is_empty() {
anyhow::bail!("Nome não pode ser vazio");
}
if !self.email.contains('@') {
anyhow::bail!("Email inválido: {}", self.email);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn usuario_valido() {
let u = Usuario::novo(1, "Maria", "maria@email.com");
assert!(u.validar().is_ok());
}
#[test]
fn email_invalido() {
let u = Usuario::novo(1, "Maria", "invalido");
assert!(u.validar().is_err());
}
}
3. Crate CLI
cargo init cli
Edite cli/Cargo.toml:
[package]
name = "meu-projeto-cli"
version.workspace = true
edition.workspace = true
[dependencies]
meu-projeto-core = { path = "../core" }
clap = { workspace = true }
serde_json = { workspace = true }
anyhow = { workspace = true }
Crie cli/src/main.rs:
use clap::Parser;
use meu_projeto_core::Usuario;
#[derive(Parser)]
#[command(name = "meu-projeto")]
#[command(about = "Gerenciador de usuários via CLI")]
enum Cli {
/// Cria um novo usuário
Criar {
#[arg(short, long)]
nome: String,
#[arg(short, long)]
email: String,
},
/// Lista usuários em formato JSON
Listar,
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
match cli {
Cli::Criar { nome, email } => {
let usuario = Usuario::novo(1, &nome, &email);
usuario.validar()?;
let json = serde_json::to_string_pretty(&usuario)?;
println!("Usuário criado:\n{}", json);
}
Cli::Listar => {
let usuarios = vec![
Usuario::novo(1, "Ana", "ana@rust.br"),
Usuario::novo(2, "Carlos", "carlos@rust.br"),
];
let json = serde_json::to_string_pretty(&usuarios)?;
println!("{}", json);
}
}
Ok(())
}
4. Crate Servidor
cargo init servidor
Edite servidor/Cargo.toml:
[package]
name = "meu-projeto-servidor"
version.workspace = true
edition.workspace = true
[dependencies]
meu-projeto-core = { path = "../core" }
axum = { workspace = true }
tokio = { workspace = true }
serde_json = { workspace = true }
Crie servidor/src/main.rs:
use axum::{routing::get, Json, Router};
use meu_projeto_core::Usuario;
async fn listar_usuarios() -> Json<Vec<Usuario>> {
let usuarios = vec![
Usuario::novo(1, "Ana", "ana@rust.br"),
Usuario::novo(2, "Carlos", "carlos@rust.br"),
];
Json(usuarios)
}
async fn saude() -> &'static str {
"OK"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/usuarios", get(listar_usuarios))
.route("/saude", get(saude));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
println!("Servidor rodando em http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
Comandos Essenciais do Workspace
Com o workspace configurado, o Cargo oferece comandos poderosos:
# Compilar todo o workspace
cargo build --workspace
# Rodar testes de todos os crates
cargo test --workspace
# Rodar apenas a CLI
cargo run -p meu-projeto-cli -- criar --nome "Ana" --email "ana@email.com"
# Rodar o servidor
cargo run -p meu-projeto-servidor
# Verificar código sem compilar
cargo check --workspace
# Rodar o Clippy em tudo
cargo clippy --workspace -- -D warnings
# Formatar todo o código
cargo fmt --all
Para mais detalhes sobre ferramentas de qualidade, confira nosso artigo sobre Clippy e Rustfmt.
Boas Práticas para Workspaces
1. Use workspace.dependencies Sempre
Centralizar dependências no Cargo.toml raiz evita versões divergentes:
[workspace.dependencies]
# Defina aqui, use com { workspace = true } nos crates
tracing = "0.1"
tracing-subscriber = "0.3"
Confira nosso guia sobre gerenciamento de dependências para estratégias avançadas.
2. Separe Lógica de Negócio de I/O
O crate core deve ser puro — sem dependências de runtime como Tokio. Isso permite reusar a lógica em CLIs, servidores, WebAssembly e testes sem carregar dependências desnecessárias.
3. Use Features para Funcionalidade Opcional
# core/Cargo.toml
[features]
default = []
persistencia = ["sqlx"]
[dependencies]
sqlx = { version = "0.8", optional = true }
Veja nosso artigo sobre compilação condicional com cfg e features para dominar esse recurso.
4. Configure CI/CD para Workspaces
No seu pipeline de CI/CD, use --workspace em todos os comandos:
steps:
- name: Check
run: cargo check --workspace --all-targets
- name: Test
run: cargo test --workspace
- name: Clippy
run: cargo clippy --workspace -- -D warnings
- name: Format
run: cargo fmt --all -- --check
5. Evite Dependências Cíclicas
Crates dentro de um workspace não podem depender um do outro de forma circular. Planeje a hierarquia:
core (sem deps internas)
↑
cli (depende de core)
servidor (depende de core)
Se dois crates precisam de funcionalidade compartilhada, extraia para um terceiro crate common ou shared.
Workspace com Padrão Default-Members
Para projetos grandes, você pode definir quais crates são compilados por padrão:
[workspace]
members = ["core", "cli", "servidor", "benchmarks", "ferramentas/*"]
default-members = ["core", "cli", "servidor"]
Assim, cargo build compila apenas os membros padrão, e cargo build --workspace compila tudo incluindo benchmarks e ferramentas auxiliares.
O padrão glob ferramentas/* é suportado e automaticamente inclui todos os crates dentro desse diretório.
Quando NÃO Usar Workspaces
Nem todo projeto precisa de workspace. Considere alternativas quando:
- Crates com ciclos de release independentes: se cada crate é publicado separadamente no crates.io com versões diferentes, um workspace pode complicar
- Projeto com menos de 2 crates: overhead desnecessário
- Equipes diferentes mantendo crates diferentes: um monorepo pode gerar conflitos
Para esses casos, dependências via crates.io com versionamento semântico podem ser mais adequadas.
Conclusão
Cargo workspaces são a ferramenta padrão para organizar projetos Rust que cresceram além de um único crate. Com dependências centralizadas, builds compartilhados e comandos unificados, eles reduzem drasticamente o atrito de manter código grande.
Se você está começando um novo projeto, considere já estruturá-lo como workspace desde o início — é muito mais fácil do que migrar depois.
Leia Também
- Cargo: O Gerenciador de Pacotes do Rust — fundamentos do Cargo
- CI/CD com Rust — pipelines para projetos Rust
- Compilação Condicional com cfg e Features — features no Cargo
- Rust 1.86 e 1.87: Trait Upcasting e Novidades — novidades recentes
- Ecossistema Rust em 2026 — visão geral do ecossistema
- Testes em Rust: Estratégias e Boas Práticas — testando seu workspace
Se você trabalha com outras linguagens que também usam monorepos, compare como Go organiza módulos e workspaces, ou como Python lida com monorepos usando ferramentas como Poetry. Para projetos de backend, Kotlin com Gradle e Zig com seu build system também oferecem abordagens interessantes para projetos grandes.