10 Projetos Práticos para Aprender Rust: Guia Completo para Iniciantes

10 Projetos Práticos para Aprender Rust: Guia Completo para Iniciantes





Projetos práticos para aprender Rust



Projeto 1 — CLI de Gerenciamento de Tarefas

Neste projeto eu foco em um CLI simples que permite adicionar, listar e marcar tarefas como concluídas. O objetivo é consolidar o fluxo de entrada/saída, persistência mínima e manejo de erros, usando clap para parsing de argumentos e serde para serialização em JSON.

  • Stack: Rust estável, cargo, clap, serde/serde_json, serde_derive (ou derive com feature).
  • Persistência: gravação de tarefas em um arquivo JSON local (tasks.json) para manter o estado entre execuções.
  • Estrutura: separação entre modelo de dados, lógica de domínio e camada de I/O.
  • Observabilidade: mensagens claras de erro e mensagens de confirmação simples para o usuário.
use clap::{Arg, Command};
use serde::{Deserialize, Serialize};
use std::fs::{self, OpenOptions};
use std::io::Write;

#[derive(Serialize, Deserialize, Debug)]
struct Tarefa {
    id: u32,
    titulo: String,
    concluida: bool,
}

fn carregar_tarefas(path: &str) -> Vec<Tarefa> {
    if let Ok(data) = fs::read_to_string(path) {
        serde_json::from_str(&data).unwrap_or_else(|_| Vec::new())
    } else {
        Vec::new()
    }
}

fn salvar_tarefas(path: &str, tarefas: &[Tarefa]) -> std::io::Result<()> {
    let json = serde_json::to_string_pretty(tarefas).unwrap();
    let mut f = OpenOptions::new().write(true).create(true).truncate(true).open(path)?;
    f.write_all(json.as_bytes())?;
    Ok(())
}

fn main() {
    let m = Command::new("taskr")
        .version("0.1.0")
        .about("Gerenciador de tarefas em CLI")
        .arg(Arg::new("add")
            .long("add")
            .about("Adiciona uma tarefa")
            .takes_value(false))
        .arg(Arg::new("name")
            .long("name")
            .about("Nome da tarefa")
            .takes_value(true))
        .get_matches();

    let path = "tasks.json";
    let mut tarefas = carregar_tarefas(path);

    if m.is_present("add") {
        if let Some(nome) = m.value_of("name") {
            let id = tarefas.iter().map(|t| t.id).max().unwrap_or(0) + 1;
            let t = Tarefa { id, titulo: nome.to_string(), concluida: false };
            tarefas.push(t);
            salvar_tarefas(path, &tarefas).expect("falha ao salvar");
            println!("Tarefa adicionada com sucesso.");
        } else {
            eprintln!("Forneça o nome da tarefa com --name .");
        }
        return;
    }

    // Listar tarefas como comportamento padrão
    println!("Tarefas ({})", tarefas.len());
    for t in &tarefas {
        println!("- [{}] #{} {}", if t.concluida { "x" } else { " " }, t.id, t.titulo);
    }
}

Observação: o objetivo aqui é que você implemente rapidamente a lógica de parseamento de argumentos, leitura/gravação simples e feedback ao usuário. Conforme evoluímos, você pode substituir a persistência por um banco leve (SQLite) ou adaptar para um formato de configuração mais complexo.

Projeto 2 — API HTTP simples com Actix Web

Este projeto apresenta uma API mínima para expor dados em formato JSON. O foco é entender o ciclo de requests, serialização/deserialização e configuração de rotas de forma clara e performática, usando Actix Web, uma das opções mais utilizadas no ecossistema Rust para serviços rápidos e estáveis.

  • End points básicos: GET /health, GET /items e POST /items com payload JSON.
  • Serialização com serde para entrada/saída de dados.
  • Estrutura simples de servidor com estado imutável para leitura de dados simulados.
use actix_web::{get, post, web, App, HttpServer, Responder, HttpResponse};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Item { id: u32, name: String }

#[get("/health")]
async fn health() -> impl Responder {
  HttpResponse::Ok().json(&{"status": "ok"})
}

#[get("/items")]
async fn list() -> impl Responder {
  let items = vec![
    Item { id: 1, name: "Item A".into() },
    Item { id: 2, name: "Item B".into() }
  ];
  HttpResponse::Ok().json(items)
}

#[post("/items")]
async fn add(item: web::Json) -> impl Responder {
  HttpResponse::Ok().json(item.0)
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
  HttpServer::new(|| {
    App::new()
      .service(health)
      .service(list)
      .service(add)
  })
  .bind("127.0.0.1:8080")?
  .run()
  .await
}

Para testar: rode o servidor e envie requisições HTTP com cURL ou um cliente de API de sua preferência. O objetivo é entender a diferença entre structs de domínio, mensagens em JSON e as rotas da aplicação.

Projeto 3 — Parser de CSV com streaming

Este projeto demonstra como ler arquivos CSV grandes sem carregá-los inteiramente na memória. Usamos o crate csv para leitura em streaming, combinando com BufRead para manter o uso de memória sob controle e processar registros linha a linha.

  • Uso eficiente de memória com streaming e lazy parsing.
  • Configuração básica do CSV: cabeçalhos, delimitação e uso de records().
  • Exemplo de processamento simples para agregar ou transformar dados ao vivo.
use std::error::Error;
use std::fs::File;
use csv::ReaderBuilder;

fn main() -> Result<(), Box<dyn Error>> {
  let f = File::open("data.csv")?;
  let mut rdr = ReaderBuilder::new()
      .has_headers(true)
      .from_reader(f);

  for result in rdr.records() {
    let record = result?;
    // Processa cada linha conforme a estrutura do seu CSV
    println!("{:?}", record);
  }
  Ok(())
}

Este padrão facilita a scalabilidade quando trabalhamos com datasets volumosos, mantendo o código simples e direto ao ponto.

Projeto 4 — Concorrência e async com Tokio

A última proposta foca em lidar com concorrência de forma eficiente em Rust, usando o runtime Tokio. Você aprende a estruturar tarefas assíncronas, sincronização básica e composição de futures, explorando cenários de I/O bound e CPU bound com utilitários simples.

  • Conceito de tasks assíncronas e await points
  • Uso de tokio::spawn para executar tarefas concorrentes
  • Coleta de resultados com join ou FuturesUnordered
#[tokio::main]
async fn main() {
  let tasks = vec![
    tokio::spawn(async { 1 + 1 }),
    tokio::spawn(async { 2 * 3 }),
  ];

  for t in tasks {
    let v = t.await.unwrap();
    println!("resultado: {}", v);
  }
}

O objetivo aqui é entender como estruturar código assíncrono de forma clara, mantendo o foco na composição de tarefas e na tratativa de erros assíncronos sem bloquear o thread principal.

Continue explorando Rust

Se você gostou deste conjunto de projetos, vale a pena mergulhar nos conteúdos adicionais que eu já produzi. Eles complementam o que você viu aqui, oferecendo fundamentos sólidos, padrões de projeto e práticas de alto desempenho.