Tonic: Framework gRPC Nativo para Rust

Guia completo do Tonic em Rust: Protocol Buffers com Prost, definição de serviços com .proto, servidor e cliente gRPC, streaming unário e bidirecional, interceptors, metadata e exemplos práticos.

Introdução

O Tonic é o framework gRPC mais maduro e popular do ecossistema Rust. Ele permite criar serviços gRPC de alta performance com suporte completo a Protocol Buffers, streaming bidirecional, interceptors, TLS e muito mais. Construído sobre Hyper e Tokio, o Tonic se integra perfeitamente ao ecossistema assíncrono do Rust.

O gRPC (Google Remote Procedure Call) é um framework de comunicação entre serviços que utiliza Protocol Buffers como formato de serialização e HTTP/2 como protocolo de transporte. Comparado com APIs REST tradicionais, o gRPC oferece serialização binária mais eficiente, contratos de API fortemente tipados, streaming bidirecional nativo e geração automática de código para múltiplas linguagens.

Por que usar Tonic?

  • Performance: serialização binária com Protocol Buffers sobre HTTP/2
  • Type safety: contratos de API verificados em tempo de compilação
  • Streaming: suporte nativo a streaming unário, de servidor, de cliente e bidirecional
  • Interoperabilidade: compatível com qualquer implementação gRPC (Go, Java, Python, etc.)
  • Geração de código: código de servidor e cliente gerado automaticamente a partir de arquivos .proto
  • Middleware: integração com Tower para interceptors e middleware composáveis

Instalação

Adicione as dependências ao seu Cargo.toml:

[dependencies]
tonic = "0.12"
prost = "0.13"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"

[build-dependencies]
tonic-build = "0.12"

O tonic-build é usado no script de build (build.rs) para compilar arquivos .proto em código Rust.

Instalando o compilador protobuf

O Tonic precisa do compilador protoc instalado no sistema:

# Ubuntu/Debian
sudo apt install protobuf-compiler

# macOS
brew install protobuf

# Arch Linux
sudo pacman -S protobuf

# Ou via cargo
cargo install protobuf-codegen

Uso Básico

Definindo o serviço com Protocol Buffers

Crie o arquivo proto/tarefas.proto na raiz do projeto:

syntax = "proto3";

package tarefas;

// Serviço de gerenciamento de tarefas
service TarefaService {
    // Operações unárias
    rpc CriarTarefa (CriarTarefaRequest) returns (Tarefa);
    rpc ObterTarefa (ObterTarefaRequest) returns (Tarefa);
    rpc ListarTarefas (ListarTarefasRequest) returns (ListarTarefasResponse);
    rpc AtualizarTarefa (AtualizarTarefaRequest) returns (Tarefa);
    rpc DeletarTarefa (DeletarTarefaRequest) returns (DeletarTarefaResponse);
}

message Tarefa {
    uint64 id = 1;
    string titulo = 2;
    string descricao = 3;
    bool concluida = 4;
    Prioridade prioridade = 5;
}

enum Prioridade {
    BAIXA = 0;
    MEDIA = 1;
    ALTA = 2;
}

message CriarTarefaRequest {
    string titulo = 1;
    string descricao = 2;
    Prioridade prioridade = 3;
}

message ObterTarefaRequest {
    uint64 id = 1;
}

message ListarTarefasRequest {
    int32 pagina = 1;
    int32 por_pagina = 2;
}

message ListarTarefasResponse {
    repeated Tarefa tarefas = 1;
    int64 total = 2;
}

message AtualizarTarefaRequest {
    uint64 id = 1;
    optional string titulo = 2;
    optional string descricao = 3;
    optional bool concluida = 4;
    optional Prioridade prioridade = 5;
}

message DeletarTarefaRequest {
    uint64 id = 1;
}

message DeletarTarefaResponse {
    bool sucesso = 1;
}

Configurando o build.rs

Crie o arquivo build.rs na raiz do projeto:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::compile_protos("proto/tarefas.proto")?;
    Ok(())
}

Para mais controle sobre a geração de código:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        .build_server(true)
        .build_client(true)
        .out_dir("src/generated")
        .compile_protos(&["proto/tarefas.proto"], &["proto"])?;
    Ok(())
}

Implementando o servidor

use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;
use tonic::{transport::Server, Request, Response, Status};

// Importar o código gerado pelo tonic-build
pub mod tarefas {
    tonic::include_proto!("tarefas");
}

use tarefas::tarefa_service_server::{TarefaService, TarefaServiceServer};
use tarefas::*;

// Estado do serviço
struct MeuServicoTarefas {
    tarefas: Arc<Mutex<HashMap<u64, Tarefa>>>,
    proximo_id: Arc<Mutex<u64>>,
}

impl MeuServicoTarefas {
    fn new() -> Self {
        Self {
            tarefas: Arc::new(Mutex::new(HashMap::new())),
            proximo_id: Arc::new(Mutex::new(1)),
        }
    }
}

#[tonic::async_trait]
impl TarefaService for MeuServicoTarefas {
    async fn criar_tarefa(
        &self,
        request: Request<CriarTarefaRequest>,
    ) -> Result<Response<Tarefa>, Status> {
        let req = request.into_inner();

        let mut proximo_id = self.proximo_id.lock().await;
        let id = *proximo_id;
        *proximo_id += 1;

        let tarefa = Tarefa {
            id,
            titulo: req.titulo,
            descricao: req.descricao,
            concluida: false,
            prioridade: req.prioridade,
        };

        self.tarefas.lock().await.insert(id, tarefa.clone());

        println!("Tarefa criada: {} (ID: {})", tarefa.titulo, id);
        Ok(Response::new(tarefa))
    }

    async fn obter_tarefa(
        &self,
        request: Request<ObterTarefaRequest>,
    ) -> Result<Response<Tarefa>, Status> {
        let id = request.into_inner().id;

        let tarefas = self.tarefas.lock().await;
        match tarefas.get(&id) {
            Some(tarefa) => Ok(Response::new(tarefa.clone())),
            None => Err(Status::not_found(format!(
                "Tarefa com ID {} não encontrada",
                id
            ))),
        }
    }

    async fn listar_tarefas(
        &self,
        request: Request<ListarTarefasRequest>,
    ) -> Result<Response<ListarTarefasResponse>, Status> {
        let req = request.into_inner();
        let pagina = req.pagina.max(1) as usize;
        let por_pagina = req.por_pagina.max(1).min(100) as usize;

        let tarefas = self.tarefas.lock().await;
        let total = tarefas.len() as i64;

        let lista: Vec<Tarefa> = tarefas
            .values()
            .skip((pagina - 1) * por_pagina)
            .take(por_pagina)
            .cloned()
            .collect();

        Ok(Response::new(ListarTarefasResponse {
            tarefas: lista,
            total,
        }))
    }

    async fn atualizar_tarefa(
        &self,
        request: Request<AtualizarTarefaRequest>,
    ) -> Result<Response<Tarefa>, Status> {
        let req = request.into_inner();

        let mut tarefas = self.tarefas.lock().await;
        match tarefas.get_mut(&req.id) {
            Some(tarefa) => {
                if let Some(titulo) = req.titulo {
                    tarefa.titulo = titulo;
                }
                if let Some(descricao) = req.descricao {
                    tarefa.descricao = descricao;
                }
                if let Some(concluida) = req.concluida {
                    tarefa.concluida = concluida;
                }
                if let Some(prioridade) = req.prioridade {
                    tarefa.prioridade = prioridade;
                }

                Ok(Response::new(tarefa.clone()))
            }
            None => Err(Status::not_found(format!(
                "Tarefa com ID {} não encontrada",
                req.id
            ))),
        }
    }

    async fn deletar_tarefa(
        &self,
        request: Request<DeletarTarefaRequest>,
    ) -> Result<Response<DeletarTarefaResponse>, Status> {
        let id = request.into_inner().id;

        let mut tarefas = self.tarefas.lock().await;
        let sucesso = tarefas.remove(&id).is_some();

        if !sucesso {
            return Err(Status::not_found(format!(
                "Tarefa com ID {} não encontrada",
                id
            )));
        }

        Ok(Response::new(DeletarTarefaResponse { sucesso }))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let servico = MeuServicoTarefas::new();

    println!("Servidor gRPC rodando em {}", addr);

    Server::builder()
        .add_service(TarefaServiceServer::new(servico))
        .serve(addr)
        .await?;

    Ok(())
}

Implementando o cliente

use tarefas::tarefa_service_client::TarefaServiceClient;
use tarefas::*;

pub mod tarefas {
    tonic::include_proto!("tarefas");
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Conectar ao servidor
    let mut client = TarefaServiceClient::connect("http://[::1]:50051").await?;

    // Criar uma tarefa
    let resposta = client
        .criar_tarefa(CriarTarefaRequest {
            titulo: "Aprender Tonic".to_string(),
            descricao: "Estudar gRPC em Rust".to_string(),
            prioridade: Prioridade::Alta as i32,
        })
        .await?;

    let tarefa = resposta.into_inner();
    println!("Tarefa criada: {:?}", tarefa);

    // Listar tarefas
    let resposta = client
        .listar_tarefas(ListarTarefasRequest {
            pagina: 1,
            por_pagina: 10,
        })
        .await?;

    let lista = resposta.into_inner();
    println!("Total de tarefas: {}", lista.total);
    for t in &lista.tarefas {
        println!("  [{}] {} - concluída: {}", t.id, t.titulo, t.concluida);
    }

    // Atualizar tarefa
    let resposta = client
        .atualizar_tarefa(AtualizarTarefaRequest {
            id: tarefa.id,
            titulo: None,
            descricao: None,
            concluida: Some(true),
            prioridade: None,
        })
        .await?;

    println!("Tarefa atualizada: {:?}", resposta.into_inner());

    // Deletar tarefa
    let resposta = client
        .deletar_tarefa(DeletarTarefaRequest { id: tarefa.id })
        .await?;

    println!("Deletada: {}", resposta.into_inner().sucesso);

    Ok(())
}

Recursos Avançados

Streaming de servidor

O servidor envia múltiplas mensagens para uma única requisição do cliente:

// No arquivo .proto
service MonitorService {
    rpc ObservarEventos (ObservarRequest) returns (stream Evento);
}

message ObservarRequest {
    string filtro = 1;
}

message Evento {
    string tipo = 1;
    string mensagem = 2;
    int64 timestamp = 3;
}
use tokio::sync::mpsc;
use tokio_stream::wrappers::ReceiverStream;
use tonic::{Request, Response, Status};

// Implementação do server streaming
#[tonic::async_trait]
impl MonitorService for MeuMonitor {
    type ObservarEventosStream = ReceiverStream<Result<Evento, Status>>;

    async fn observar_eventos(
        &self,
        request: Request<ObservarRequest>,
    ) -> Result<Response<Self::ObservarEventosStream>, Status> {
        let filtro = request.into_inner().filtro;
        println!("Cliente observando eventos com filtro: {}", filtro);

        let (tx, rx) = mpsc::channel(128);

        // Spawn uma task que envia eventos periodicamente
        tokio::spawn(async move {
            let mut contador = 0;
            loop {
                contador += 1;
                let evento = Evento {
                    tipo: "info".to_string(),
                    mensagem: format!("Evento #{} (filtro: {})", contador, filtro),
                    timestamp: chrono::Utc::now().timestamp(),
                };

                if tx.send(Ok(evento)).await.is_err() {
                    // Cliente desconectou
                    break;
                }

                tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            }
        });

        Ok(Response::new(ReceiverStream::new(rx)))
    }
}

Streaming de cliente

O cliente envia múltiplas mensagens e recebe uma única resposta:

service UploadService {
    rpc EnviarDados (stream DadosChunk) returns (UploadResposta);
}

message DadosChunk {
    bytes conteudo = 1;
    int32 numero_chunk = 2;
}

message UploadResposta {
    int64 bytes_recebidos = 1;
    int32 chunks_processados = 2;
}
use tokio_stream::StreamExt;
use tonic::{Request, Response, Status, Streaming};

#[tonic::async_trait]
impl UploadService for MeuUpload {
    async fn enviar_dados(
        &self,
        request: Request<Streaming<DadosChunk>>,
    ) -> Result<Response<UploadResposta>, Status> {
        let mut stream = request.into_inner();
        let mut bytes_total: i64 = 0;
        let mut chunks = 0;

        while let Some(chunk) = stream.next().await {
            let chunk = chunk?;
            bytes_total += chunk.conteudo.len() as i64;
            chunks += 1;
            println!(
                "Chunk #{}: {} bytes recebidos",
                chunk.numero_chunk,
                chunk.conteudo.len()
            );
        }

        println!(
            "Upload concluído: {} bytes em {} chunks",
            bytes_total, chunks
        );

        Ok(Response::new(UploadResposta {
            bytes_recebidos: bytes_total,
            chunks_processados: chunks,
        }))
    }
}

Streaming bidirecional

Ambos cliente e servidor enviam e recebem mensagens simultaneamente:

service ChatService {
    rpc Chat (stream ChatMensagem) returns (stream ChatMensagem);
}

message ChatMensagem {
    string usuario = 1;
    string texto = 2;
    int64 timestamp = 3;
}
use tokio::sync::mpsc;
use tokio_stream::{wrappers::ReceiverStream, StreamExt};
use tonic::{Request, Response, Status, Streaming};

#[tonic::async_trait]
impl ChatService for MeuChat {
    type ChatStream = ReceiverStream<Result<ChatMensagem, Status>>;

    async fn chat(
        &self,
        request: Request<Streaming<ChatMensagem>>,
    ) -> Result<Response<Self::ChatStream>, Status> {
        let mut stream_entrada = request.into_inner();
        let (tx, rx) = mpsc::channel(128);

        tokio::spawn(async move {
            while let Some(resultado) = stream_entrada.next().await {
                match resultado {
                    Ok(msg) => {
                        println!("[{}]: {}", msg.usuario, msg.texto);

                        // Eco: enviar resposta de volta
                        let resposta = ChatMensagem {
                            usuario: "Servidor".to_string(),
                            texto: format!("Recebi: '{}'", msg.texto),
                            timestamp: chrono::Utc::now().timestamp(),
                        };

                        if tx.send(Ok(resposta)).await.is_err() {
                            break;
                        }
                    }
                    Err(e) => {
                        eprintln!("Erro no stream: {}", e);
                        break;
                    }
                }
            }
        });

        Ok(Response::new(ReceiverStream::new(rx)))
    }
}

Interceptors (middleware)

use tonic::{
    metadata::MetadataValue,
    transport::Server,
    Request, Status,
};

// Interceptor de autenticação
fn autenticar(req: Request<()>) -> Result<Request<()>, Status> {
    let token = req
        .metadata()
        .get("authorization")
        .and_then(|v| v.to_str().ok());

    match token {
        Some(t) if t.starts_with("Bearer ") => {
            let jwt = &t[7..];
            // Aqui você validaria o JWT
            if jwt == "token-valido" {
                Ok(req)
            } else {
                Err(Status::unauthenticated("Token inválido"))
            }
        }
        _ => Err(Status::unauthenticated(
            "Token de autenticação ausente. Envie via header 'authorization: Bearer <token>'"
        )),
    }
}

// Interceptor de logging
fn logging(req: Request<()>) -> Result<Request<()>, Status> {
    let metodo = req.metadata().get("grpc-method")
        .and_then(|v| v.to_str().ok())
        .unwrap_or("desconhecido");

    println!("[LOG] Requisição recebida: {}", metodo);
    Ok(req)
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let servico = MeuServicoTarefas::new();

    // Aplicar interceptors ao serviço
    let servico_com_auth = TarefaServiceServer::with_interceptor(
        servico,
        autenticar,
    );

    Server::builder()
        .add_service(servico_com_auth)
        .serve(addr)
        .await?;

    Ok(())
}

Metadata (cabeçalhos gRPC)

use tonic::{metadata::MetadataValue, Request};

// Cliente enviando metadata
async fn enviar_com_metadata(
    client: &mut TarefaServiceClient<tonic::transport::Channel>,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut request = Request::new(ListarTarefasRequest {
        pagina: 1,
        por_pagina: 10,
    });

    // Adicionar metadata (cabeçalhos)
    request.metadata_mut().insert(
        "authorization",
        MetadataValue::try_from("Bearer meu-token")?,
    );
    request.metadata_mut().insert(
        "x-request-id",
        MetadataValue::try_from("req-12345")?,
    );

    let resposta = client.listar_tarefas(request).await?;

    // Ler metadata da resposta
    let headers = resposta.metadata();
    if let Some(versao) = headers.get("x-api-version") {
        println!("Versão da API: {}", versao.to_str()?);
    }

    println!("Tarefas: {:?}", resposta.into_inner());
    Ok(())
}

TLS e segurança

use tonic::transport::{Certificate, Identity, Server, ServerTlsConfig};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let cert = tokio::fs::read("server.pem").await?;
    let key = tokio::fs::read("server-key.pem").await?;
    let ca_cert = tokio::fs::read("ca.pem").await?;

    let identity = Identity::from_pem(cert, key);
    let ca_certificado = Certificate::from_pem(ca_cert);

    let tls_config = ServerTlsConfig::new()
        .identity(identity)
        .client_ca_root(ca_certificado); // mTLS

    let addr = "[::1]:50051".parse()?;
    let servico = MeuServicoTarefas::new();

    Server::builder()
        .tls_config(tls_config)?
        .add_service(TarefaServiceServer::new(servico))
        .serve(addr)
        .await?;

    Ok(())
}

Boas Práticas

1. Estruture o projeto corretamente

meu-servico-grpc/
  proto/
    tarefas.proto
  src/
    main.rs
    server.rs
    client.rs
  build.rs
  Cargo.toml

2. Use erros gRPC apropriados

use tonic::Status;

// Mapeie erros de domínio para Status gRPC
fn mapear_erro(erro: MeuErro) -> Status {
    match erro {
        MeuErro::NaoEncontrado(msg) => Status::not_found(msg),
        MeuErro::Validacao(msg) => Status::invalid_argument(msg),
        MeuErro::NaoAutorizado => Status::unauthenticated("Acesso negado"),
        MeuErro::Proibido => Status::permission_denied("Sem permissão"),
        MeuErro::Conflito(msg) => Status::already_exists(msg),
        MeuErro::Interno(msg) => {
            eprintln!("Erro interno: {}", msg);
            Status::internal("Erro interno do servidor")
        }
    }
}

3. Implemente health checking

// Use o protocolo padrão de health check do gRPC
syntax = "proto3";

package grpc.health.v1;

service Health {
    rpc Check (HealthCheckRequest) returns (HealthCheckResponse);
    rpc Watch (HealthCheckRequest) returns (stream HealthCheckResponse);
}

message HealthCheckRequest {
    string service = 1;
}

message HealthCheckResponse {
    enum ServingStatus {
        UNKNOWN = 0;
        SERVING = 1;
        NOT_SERVING = 2;
    }
    ServingStatus status = 1;
}

4. Configure timeouts e limites

use std::time::Duration;
use tonic::transport::Server;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;

    Server::builder()
        .timeout(Duration::from_secs(30))
        .concurrency_limit_per_connection(256)
        .add_service(TarefaServiceServer::new(MeuServicoTarefas::new()))
        .serve(addr)
        .await?;

    Ok(())
}

5. Use Reflection para depuração

[dependencies]
tonic-reflection = "0.12"
use tonic_reflection::server::Builder;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let reflection_service = Builder::configure()
        .register_encoded_file_descriptor_set(tarefas::FILE_DESCRIPTOR_SET)
        .build_v1()?;

    Server::builder()
        .add_service(TarefaServiceServer::new(MeuServicoTarefas::new()))
        .add_service(reflection_service)
        .serve("[::1]:50051".parse()?)
        .await?;

    Ok(())
}

Exemplos Práticos

Microserviço gRPC completo com health check

use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
use tonic::{transport::Server, Request, Response, Status};

pub mod tarefas {
    tonic::include_proto!("tarefas");
}

use tarefas::tarefa_service_server::{TarefaService, TarefaServiceServer};
use tarefas::*;

// ---- Camada de domínio ----

#[derive(Clone, Debug)]
struct TarefaDominio {
    id: u64,
    titulo: String,
    descricao: String,
    concluida: bool,
    prioridade: i32,
}

impl From<TarefaDominio> for Tarefa {
    fn from(t: TarefaDominio) -> Self {
        Tarefa {
            id: t.id,
            titulo: t.titulo,
            descricao: t.descricao,
            concluida: t.concluida,
            prioridade: t.prioridade,
        }
    }
}

// ---- Repositório ----

#[derive(Clone)]
struct TarefaRepo {
    dados: Arc<RwLock<HashMap<u64, TarefaDominio>>>,
    proximo_id: Arc<RwLock<u64>>,
}

impl TarefaRepo {
    fn new() -> Self {
        Self {
            dados: Arc::new(RwLock::new(HashMap::new())),
            proximo_id: Arc::new(RwLock::new(1)),
        }
    }

    async fn criar(&self, titulo: String, descricao: String, prioridade: i32) -> TarefaDominio {
        let mut proximo = self.proximo_id.write().await;
        let id = *proximo;
        *proximo += 1;

        let tarefa = TarefaDominio {
            id,
            titulo,
            descricao,
            concluida: false,
            prioridade,
        };

        self.dados.write().await.insert(id, tarefa.clone());
        tarefa
    }

    async fn obter(&self, id: u64) -> Option<TarefaDominio> {
        self.dados.read().await.get(&id).cloned()
    }

    async fn listar(&self, pagina: usize, por_pagina: usize) -> (Vec<TarefaDominio>, usize) {
        let dados = self.dados.read().await;
        let total = dados.len();
        let lista: Vec<_> = dados
            .values()
            .skip((pagina - 1) * por_pagina)
            .take(por_pagina)
            .cloned()
            .collect();
        (lista, total)
    }

    async fn deletar(&self, id: u64) -> bool {
        self.dados.write().await.remove(&id).is_some()
    }
}

// ---- Serviço gRPC ----

struct ServicoTarefas {
    repo: TarefaRepo,
}

#[tonic::async_trait]
impl TarefaService for ServicoTarefas {
    async fn criar_tarefa(
        &self,
        request: Request<CriarTarefaRequest>,
    ) -> Result<Response<Tarefa>, Status> {
        let req = request.into_inner();

        if req.titulo.is_empty() {
            return Err(Status::invalid_argument("Título não pode ser vazio"));
        }
        if req.titulo.len() > 200 {
            return Err(Status::invalid_argument("Título muito longo (máx 200 chars)"));
        }

        let tarefa = self
            .repo
            .criar(req.titulo, req.descricao, req.prioridade)
            .await;

        Ok(Response::new(tarefa.into()))
    }

    async fn obter_tarefa(
        &self,
        request: Request<ObterTarefaRequest>,
    ) -> Result<Response<Tarefa>, Status> {
        let id = request.into_inner().id;

        self.repo
            .obter(id)
            .await
            .map(|t| Response::new(t.into()))
            .ok_or_else(|| Status::not_found(format!("Tarefa {} não encontrada", id)))
    }

    async fn listar_tarefas(
        &self,
        request: Request<ListarTarefasRequest>,
    ) -> Result<Response<ListarTarefasResponse>, Status> {
        let req = request.into_inner();
        let pagina = (req.pagina as usize).max(1);
        let por_pagina = (req.por_pagina as usize).clamp(1, 100);

        let (tarefas, total) = self.repo.listar(pagina, por_pagina).await;

        Ok(Response::new(ListarTarefasResponse {
            tarefas: tarefas.into_iter().map(Into::into).collect(),
            total: total as i64,
        }))
    }

    async fn atualizar_tarefa(
        &self,
        request: Request<AtualizarTarefaRequest>,
    ) -> Result<Response<Tarefa>, Status> {
        let req = request.into_inner();
        let mut dados = self.repo.dados.write().await;

        match dados.get_mut(&req.id) {
            Some(tarefa) => {
                if let Some(titulo) = req.titulo {
                    tarefa.titulo = titulo;
                }
                if let Some(descricao) = req.descricao {
                    tarefa.descricao = descricao;
                }
                if let Some(concluida) = req.concluida {
                    tarefa.concluida = concluida;
                }
                if let Some(prioridade) = req.prioridade {
                    tarefa.prioridade = prioridade;
                }
                Ok(Response::new(tarefa.clone().into()))
            }
            None => Err(Status::not_found(format!(
                "Tarefa {} não encontrada", req.id
            ))),
        }
    }

    async fn deletar_tarefa(
        &self,
        request: Request<DeletarTarefaRequest>,
    ) -> Result<Response<DeletarTarefaResponse>, Status> {
        let id = request.into_inner().id;
        let sucesso = self.repo.deletar(id).await;

        if !sucesso {
            return Err(Status::not_found(format!(
                "Tarefa {} não encontrada", id
            )));
        }

        Ok(Response::new(DeletarTarefaResponse { sucesso }))
    }
}

// ---- Ponto de entrada ----

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "[::1]:50051".parse()?;
    let repo = TarefaRepo::new();
    let servico = ServicoTarefas { repo };

    println!("Microserviço gRPC de Tarefas");
    println!("Endereço: {}", addr);

    Server::builder()
        .add_service(TarefaServiceServer::new(servico))
        .serve(addr)
        .await?;

    Ok(())
}

Comparação com Alternativas

CaracterísticaTonic (gRPC)Axum (REST)tarpc (RPC)
ProtocoloHTTP/2 + ProtobufHTTP/1.1 ou /2 + JSONTCP customizado
SerializaçãoProtocol Buffers (binário)JSON (texto)Bincode/Serde
StreamingNativo (4 tipos)Via WebSocket/SSELimitado
InteroperabilidadeMulti-linguagemMulti-linguagemSomente Rust
ContratoArquivo .protoOpenAPI/SwaggerTrait Rust
Geração de códigoAutomática (build.rs)Não necessáriaMacro proc
PerformanceExcelenteBoaExcelente
EcossistemaGrande (multi-lang)Enorme (HTTP)Pequeno
  • Tonic vs REST (Axum): Use gRPC quando precisa de comunicação eficiente entre serviços internos. Use REST para APIs públicas e frontends web.
  • Tonic vs tarpc: tarpc é mais simples e ideal para comunicação entre serviços Rust-to-Rust. Tonic é melhor quando precisa de interoperabilidade com outras linguagens.
  • Cenário ideal para gRPC: microserviços internos com alto volume de comunicação, streaming de dados, e equipes usando múltiplas linguagens.

Conclusão

O Tonic torna o desenvolvimento de serviços gRPC em Rust acessível e produtivo. Com geração automática de código a partir de arquivos .proto, integração com o ecossistema Tokio/Tower e suporte completo a streaming, ele é a escolha natural para comunicação entre microserviços em Rust.

O gRPC brilha especialmente em cenários de comunicação interna entre serviços, onde a eficiência do Protocol Buffers e a tipagem forte dos contratos .proto proporcionam ganhos significativos de performance e confiabilidade.

Próximos passos

  • Estude Tower para adicionar middleware (autenticação, rate limiting, logging) aos seus serviços gRPC
  • Explore Protocol Buffers para modelar dados complexos
  • Configure Tracing para observabilidade dos seus serviços gRPC
  • Aprenda sobre Service Mesh e como o gRPC se integra com ferramentas como Istio e Linkerd