Cargo Workspaces: Organizando Projetos Grandes em Rust — 2026

Aprenda a usar Cargo workspaces para organizar monorepos em Rust. Dependências compartilhadas, builds otimizados e boas práticas para projetos grandes.

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 usa serde 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.lock para todo o projeto
  • Cache de compilação compartilhado no mesmo diretório target/
  • Dependências unificadas com workspace.dependencies
  • Comandos como cargo test --workspace que 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

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.