Portfólio no GitHub para Desenvolvedores Rust

Como montar um portfólio GitHub que impressiona recrutadores de vagas Rust. Projetos recomendados, estrutura de README, CI/CD com GitHub Actions, profile README e estratégias de contribuição.

Portfólio no GitHub para Desenvolvedores Rust

Seu perfil no GitHub é, para muitos recrutadores e tech leads de Rust, mais importante que o próprio currículo. Em uma linguagem onde a comunidade open source é central e o código fala por si, um GitHub bem organizado pode abrir portas que nenhum diploma consegue. Este guia ensina como transformar seu perfil GitHub em uma vitrine profissional, desde a escolha dos projetos até a configuração de CI/CD e a otimização do seu profile README.


Por Que o GitHub é Tão Importante para Vagas Rust

A comunidade Rust nasceu no open source e valoriza profundamente a qualidade de código. Quando um recrutador ou tech lead avalia um candidato, o GitHub responde a perguntas que o currículo não consegue:

  • Essa pessoa realmente sabe Rust? O código mostra a verdade
  • Como ela lida com erros? Usa unwrap() em tudo ou trata erros adequadamente?
  • Ela escreve testes? Tem suite de testes organizada?
  • Como organiza o código? A estrutura de módulos faz sentido?
  • Ela documenta? READMEs e doc comments estão presentes?
  • Ela colabora? Tem PRs em outros projetos, responde issues?

Projetos que Impressionam Recrutadores

Critérios de um bom projeto de portfólio

Um projeto que vai impressionar precisa ter:

  1. Propósito claro: resolve um problema real ou demonstra uma habilidade específica
  2. Código limpo e idiomático: segue as convenções da comunidade Rust
  3. Testes: unitários, de integração e possivelmente benchmarks
  4. Documentação: README completo, doc comments, exemplos de uso
  5. CI/CD: builds e testes automatizados que passam
  6. Commits organizados: histórico legível com mensagens descritivas

Projetos recomendados por nível

Para Júnior (3-4 projetos)

ProjetoO que demonstraComplexidade
CLI tool (grep, wc, find)I/O, args parsing, error handlingBaixa
API REST com banco de dadosWeb, async, SQL, serializaçãoMédia
Chat TCP/UDPNetworking, concorrência, protocolosMédia
Conversor/Parser de formatosParsing, serde, file I/OBaixa-Média

Para Pleno (2-3 projetos mais substanciais)

ProjetoO que demonstraComplexidade
Microserviço completo com DockerArquitetura, deployment, observabilityAlta
Biblioteca publicada no crates.ioDesign de API, documentação, versionamentoAlta
Sistema de cache/storageEstruturas de dados, performance, benchmarksAlta

Para Sênior (1-2 projetos ambiciosos)

ProjetoO que demonstraComplexidade
Engine/Runtime/CompiladorConhecimento profundo, design de sistemasMuito alta
Contribuições significativas a projetos popularesColaboração, código de produçãoVariável

Exemplo de projeto CLI impressionante

Veja como estruturar um projeto que demonstra competência:

meu-projeto-cli/
├── Cargo.toml
├── README.md
├── LICENSE
├── .github/
│   └── workflows/
│       └── ci.yml
├── src/
│   ├── main.rs
│   ├── lib.rs
│   ├── cli.rs
│   ├── config.rs
│   ├── error.rs
│   └── processor/
│       ├── mod.rs
│       ├── parser.rs
│       └── formatter.rs
├── tests/
│   ├── integration_test.rs
│   └── fixtures/
│       ├── input_valido.txt
│       └── input_invalido.txt
├── benches/
│   └── benchmark.rs
└── examples/
    └── uso_basico.rs

Arquivo src/error.rs demonstrando tratamento de erros idiomático:

use std::fmt;
use std::io;

/// Erros possíveis durante o processamento de arquivos
#[derive(Debug)]
pub enum AppError {
    /// Erro de leitura/escrita de arquivo
    Io(io::Error),
    /// Formato de entrada inválido
    FormatoInvalido {
        linha: usize,
        detalhes: String,
    },
    /// Configuração ausente ou inválida
    Config(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "Erro de I/O: {}", e),
            AppError::FormatoInvalido { linha, detalhes } => {
                write!(f, "Formato inválido na linha {}: {}", linha, detalhes)
            }
            AppError::Config(msg) => write!(f, "Erro de configuração: {}", msg),
        }
    }
}

impl std::error::Error for AppError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<io::Error> for AppError {
    fn from(e: io::Error) -> Self {
        AppError::Io(e)
    }
}

Arquivo src/lib.rs com documentação e testes:

//! # Meu Processador de Dados
//!
//! Biblioteca para processamento e transformação de arquivos
//! de dados estruturados.
//!
//! ## Exemplo de uso
//!
//! ```rust
//! use meu_processador::Processador;
//!
//! let proc = Processador::new();
//! let resultado = proc.processar("dados.csv").unwrap();
//! println!("Processados {} registros", resultado.total);
//! ```

pub mod config;
pub mod error;
pub mod processor;

pub use error::AppError;

/// Resultado do processamento contendo estatísticas
#[derive(Debug, Clone)]
pub struct Resultado {
    /// Total de registros processados
    pub total: usize,
    /// Registros que passaram na validação
    pub validos: usize,
    /// Registros com erros
    pub invalidos: usize,
    /// Tempo de processamento em milissegundos
    pub tempo_ms: u128,
}

/// Processador principal de dados
pub struct Processador {
    config: config::Config,
}

impl Processador {
    /// Cria um novo processador com configuração padrão
    pub fn new() -> Self {
        Self {
            config: config::Config::default(),
        }
    }

    /// Cria um processador com configuração customizada
    pub fn with_config(config: config::Config) -> Self {
        Self { config }
    }

    /// Processa um arquivo e retorna o resultado
    ///
    /// # Errors
    ///
    /// Retorna `AppError::Io` se o arquivo não puder ser lido
    /// Retorna `AppError::FormatoInvalido` se o formato for inválido
    pub fn processar(&self, caminho: &str) -> Result<Resultado, AppError> {
        let inicio = std::time::Instant::now();
        let conteudo = std::fs::read_to_string(caminho)?;

        let mut validos = 0;
        let mut invalidos = 0;

        for (i, linha) in conteudo.lines().enumerate() {
            if self.validar_linha(linha) {
                validos += 1;
            } else {
                invalidos += 1;
                if self.config.strict {
                    return Err(AppError::FormatoInvalido {
                        linha: i + 1,
                        detalhes: format!("Linha inválida: {}", linha),
                    });
                }
            }
        }

        Ok(Resultado {
            total: validos + invalidos,
            validos,
            invalidos,
            tempo_ms: inicio.elapsed().as_millis(),
        })
    }

    fn validar_linha(&self, linha: &str) -> bool {
        !linha.trim().is_empty() && linha.contains(self.config.delimitador)
    }
}

impl Default for Processador {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use tempfile::NamedTempFile;

    fn criar_arquivo_temp(conteudo: &str) -> NamedTempFile {
        let mut arquivo = NamedTempFile::new().unwrap();
        write!(arquivo, "{}", conteudo).unwrap();
        arquivo
    }

    #[test]
    fn test_processar_arquivo_valido() {
        let arquivo = criar_arquivo_temp("campo1,campo2\nvalor1,valor2\n");
        let proc = Processador::new();
        let resultado = proc.processar(arquivo.path().to_str().unwrap()).unwrap();

        assert_eq!(resultado.total, 2);
        assert_eq!(resultado.validos, 2);
        assert_eq!(resultado.invalidos, 0);
    }

    #[test]
    fn test_processar_com_linhas_invalidas() {
        let arquivo = criar_arquivo_temp("campo1,campo2\nlinha_sem_virgula\n");
        let proc = Processador::new();
        let resultado = proc.processar(arquivo.path().to_str().unwrap()).unwrap();

        assert_eq!(resultado.total, 2);
        assert_eq!(resultado.validos, 1);
        assert_eq!(resultado.invalidos, 1);
    }

    #[test]
    fn test_arquivo_inexistente() {
        let proc = Processador::new();
        let resultado = proc.processar("/caminho/inexistente.csv");

        assert!(resultado.is_err());
        assert!(matches!(resultado, Err(AppError::Io(_))));
    }
}

Estrutura de README para Projetos Rust

O README é a primeira coisa que as pessoas veem. Siga esta estrutura:

Template de README

# Nome do Projeto

Breve descrição em uma frase do que o projeto faz.

[![CI](https://github.com/usuario/projeto/actions/workflows/ci.yml/badge.svg)](https://github.com/usuario/projeto/actions)
[![Crates.io](https://img.shields.io/crates/v/nome-do-crate)](https://crates.io/crates/nome-do-crate)
[![Documentation](https://docs.rs/nome-do-crate/badge.svg)](https://docs.rs/nome-do-crate)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Funcionalidades

- Funcionalidade 1: descrição breve
- Funcionalidade 2: descrição breve
- Funcionalidade 3: descrição breve

## Instalação

### Via cargo

    cargo install nome-do-projeto

### Compilar do fonte

    git clone https://github.com/usuario/projeto
    cd projeto
    cargo build --release

## Uso

### Exemplo básico

    nome-do-projeto arquivo.txt --opcao valor

### Como biblioteca

    use nome_do_projeto::Processador;

    let proc = Processador::new();
    let resultado = proc.processar("dados.csv")?;

## Arquitetura

Breve descrição da arquitetura do projeto.

## Contribuindo

Contribuições são bem-vindas! Veja CONTRIBUTING.md.

## Licença

MIT - veja LICENSE.

Dicas para um README eficaz

  1. Comece com o propósito: o que o projeto faz e por que ele existe
  2. Inclua badges: CI status, versão no crates.io, licença
  3. Mostre exemplos de uso: código funcional que pode ser copiado e rodado
  4. Documente a instalação: passos claros para começar a usar
  5. Inclua screenshots/GIFs: para ferramentas CLI, mostre o output
  6. Mantenha atualizado: README desatualizado é pior que README simples

CI/CD com GitHub Actions para Rust

Um projeto com CI configurado demonstra profissionalismo. Este é um workflow completo:

Workflow básico (recomendado para todos os projetos)

Crie o arquivo .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  CARGO_TERM_COLOR: always
  RUSTFLAGS: -Dwarnings

jobs:
  check:
    name: Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo check --all-features

  fmt:
    name: Rustfmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: rustfmt
      - run: cargo fmt --all -- --check

  clippy:
    name: Clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@v2
      - run: cargo clippy --all-targets --all-features

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --all-features

  docs:
    name: Docs
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - run: cargo doc --no-deps --all-features
        env:
          RUSTDOCFLAGS: -Dwarnings

Workflow avançado (para projetos mais maduros)

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

env:
  CARGO_TERM_COLOR: always

jobs:
  test:
    name: Test (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        rust: [stable, beta]
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: ${{ matrix.rust }}
      - uses: Swatinem/rust-cache@v2
      - run: cargo test --all-features --verbose

  msrv:
    name: Minimum Supported Rust Version
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.75.0"  # seu MSRV
      - uses: Swatinem/rust-cache@v2
      - run: cargo check --all-features

  coverage:
    name: Code Coverage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Install cargo-tarpaulin
        run: cargo install cargo-tarpaulin
      - name: Generate coverage
        run: cargo tarpaulin --out xml
      - name: Upload to codecov
        uses: codecov/codecov-action@v3
        with:
          file: cobertura.xml

  security:
    name: Security Audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

Workflow de release automatizado

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    name: Build (${{ matrix.target }})
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - run: cargo build --release --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/meu-projeto*

  publish:
    name: Publish to crates.io
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo publish
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}

Profile README: Sua Vitrine Pessoal

O Profile README aparece na página principal do seu GitHub. Crie um repositório com seu nome de usuário e adicione um README.md.

Exemplo de Profile README para dev Rust

# Olá! Sou [Seu Nome] 🦀

Desenvolvedor(a) Rust apaixonado(a) por sistemas de alta performance
e código seguro.

## Sobre mim

- Desenvolvedor(a) Rust com X anos de experiência
- Foco em backend, sistemas distribuídos e ferramentas CLI
- Contribuidor(a) open source ativo(a)
- Baseado(a) em [Cidade], Brasil

## Tecnologias

**Principal:** Rust

**Ecossistema Rust:** Tokio, Axum, Serde, SQLx, Clap, Tracing

**Também trabalho com:** PostgreSQL, Redis, Docker, AWS, GitHub Actions

## Projetos em destaque

| Projeto | Descrição | Status |
|---------|-----------|--------|
| [projeto-1](link) | Descrição curta | Em desenvolvimento |
| [projeto-2](link) | Descrição curta | Estável |
| [projeto-3](link) | Descrição curta | Publicado no crates.io |

## Contribuições Open Source

- [tokio](https://github.com/tokio-rs/tokio) - Melhoria de performance em X
- [serde](https://github.com/serde-rs/serde) - Documentação e exemplos

## Contato

- LinkedIn: [seu-linkedin](link)
- Blog: [seu-blog](link)
- Email: seu@email.com

Dicas para o Profile README

  1. Seja conciso: ninguém vai ler um README de 200 linhas no seu perfil
  2. Destaque Rust: deixe claro que é sua linguagem principal
  3. Link para projetos: facilite a navegação até seus melhores trabalhos
  4. Mantenha atualizado: perfil desatualizado passa má impressão
  5. Evite widgets excessivos: badges de streak e stats ficam poluídos se forem muitos
  6. Mostre personalidade: é um espaço profissional, mas pode ter seu toque pessoal

Organizando Seus Repositórios

Fixar repositórios estratégicos

O GitHub permite fixar até 6 repositórios no seu perfil. Escolha estrategicamente:

  1. Seu melhor projeto Rust completo (com testes, CI, README)
  2. Uma contribuição a projeto popular (fork com PR aceito)
  3. Uma biblioteca/crate publicada (se tiver)
  4. Um projeto que demonstre habilidade específica (async, CLI, web)
  5. Profile README (se estiver bem feito)
  6. Algo criativo ou divertido (demonstra paixão)

Limpeza de repositórios

  • Archive projetos antigos ou abandonados que não representam seu nível atual
  • Torne privados exercícios de curso ou código de baixa qualidade
  • Delete forks que você nunca modificou
  • Adicione descrições em todos os repositórios públicos
  • Adicione tópicos/tags (rust, cli, web, async, etc.)

Gráfico de Contribuições e Atividade

O que recrutadores olham

O gráfico de contribuições (os quadradinhos verdes) não é tudo, mas conta uma história:

  • Consistência: atividade regular é melhor que picos esporádicos
  • Tipo de atividade: commits, PRs, issues e reviews contam
  • Profundidade: contribuições significativas pesam mais que commits triviais

Estratégias para manter atividade consistente

  1. Comprometa-se com 30 minutos por dia: pequenos commits diários mantêm o gráfico verde
  2. Tenha um projeto ativo: sempre trabalhe em pelo menos um projeto pessoal
  3. Revise issues em projetos que usa: triagem de issues conta como contribuição
  4. Documente seu aprendizado: commits de documentação também contam
  5. Participe de code reviews: review de PRs de terceiros mostra colaboração

O que NÃO fazer

  • Não faça commits vazios ou artificiais só para deixar o gráfico verde
  • Não commite código ruim apressadamente
  • Não force atividade quando não há o que contribuir genuinamente
  • Não se estresse com o gráfico; qualidade importa mais

Escrevendo Doc Comments de Qualidade

Doc comments em Rust aparecem na documentação gerada pelo cargo doc e no docs.rs. Escrever boa documentação é um diferencial enorme.

/// Calcula a média ponderada de um conjunto de valores.
///
/// Cada valor é acompanhado de um peso. A média é calculada como
/// a soma dos produtos (valor * peso) dividida pela soma dos pesos.
///
/// # Arguments
///
/// * `valores` - Slice de tuplas (valor, peso)
///
/// # Returns
///
/// Retorna `Some(media)` se houver valores, ou `None` se o slice
/// estiver vazio ou a soma dos pesos for zero.
///
/// # Examples
///
/// ```
/// use minha_lib::media_ponderada;
///
/// let valores = vec![(8.0, 2.0), (9.0, 3.0), (7.0, 1.0)];
/// let media = media_ponderada(&valores).unwrap();
/// assert!((media - 8.33).abs() < 0.01);
/// ```
///
/// ```
/// use minha_lib::media_ponderada;
///
/// // Retorna None para entrada vazia
/// assert_eq!(media_ponderada(&[]), None);
/// ```
pub fn media_ponderada(valores: &[(f64, f64)]) -> Option<f64> {
    if valores.is_empty() {
        return None;
    }

    let soma_ponderada: f64 = valores.iter()
        .map(|(v, p)| v * p)
        .sum();

    let soma_pesos: f64 = valores.iter()
        .map(|(_, p)| *p)
        .sum();

    if soma_pesos == 0.0 {
        None
    } else {
        Some(soma_ponderada / soma_pesos)
    }
}

Publicando no crates.io

Ter uma crate publicada no crates.io é um excelente diferencial.

Checklist antes de publicar

# Cargo.toml completo para publicação
[package]
name = "minha-crate"
version = "0.1.0"
edition = "2021"
authors = ["Seu Nome <seu@email.com>"]
description = "Descrição curta e clara da crate"
license = "MIT"
repository = "https://github.com/usuario/minha-crate"
homepage = "https://github.com/usuario/minha-crate"
documentation = "https://docs.rs/minha-crate"
readme = "README.md"
keywords = ["rust", "keyword2", "keyword3"]
categories = ["command-line-utilities"]
rust-version = "1.75.0"  # MSRV

[badges]
maintenance = { status = "actively-developed" }

Passos para publicar

# 1. Verificar se tudo está correto
cargo publish --dry-run

# 2. Rodar todos os testes
cargo test --all-features

# 3. Verificar a documentação
cargo doc --open

# 4. Publicar
cargo publish

Erros Comuns no Portfólio GitHub

  1. Repositórios sem README: um projeto sem documentação é quase invisível
  2. CI quebrado: badge vermelha de CI é pior que não ter badge
  3. Código com unwrap() em toda parte: demonstra descuido com tratamento de erros
  4. Sem testes: código sem testes levanta bandeiras vermelhas
  5. Commits tipo “fix”, “update”, “asdf”: histórico de commits ilegível
  6. Muitos forks sem modificação: polui o perfil sem agregar valor
  7. Projetos todos iguais: diversifique (CLI, web, lib, contribuição)
  8. Ignorar warnings do Clippy: ative -Dwarnings no CI
  9. Dependências desatualizadas: mostre que mantém o projeto saudável
  10. Não ter licença: sem licença, ninguém pode usar seu código legalmente

Conclusão

Seu portfólio no GitHub é a prova concreta das suas habilidades. Para construir um perfil que impressiona:

  • Qualidade sobre quantidade: 3 projetos excelentes valem mais que 20 mediocres
  • Código idiomático: siga as convenções da comunidade Rust
  • Documentação completa: README, doc comments, exemplos
  • CI/CD configurado: demonstre profissionalismo com automação
  • Contribuições open source: mostre que sabe colaborar
  • Profile README otimizado: crie uma boa primeira impressão
  • Consistência: mantenha atividade regular e projetos atualizados

Invista tempo em polir seus melhores projetos. Cada repositório pinado é uma oportunidade de mostrar ao mundo o que você sabe fazer com Rust.