Rust para Backend: Guia Pratico
Introdução
Você já tem seus serviços rodando em Node.js com TypeScript. A API responde bem, o time conhece o ecossistema, e o deploy funciona. Então por que considerar Rust para o backend?
A resposta curta: porque existem problemas que Node.js resolve com esforço e Rust resolve por design. Latência previsível sem garbage collector, segurança de memória garantida em tempo de compilação, e um consumo de recursos que faz seu time de infraestrutura sorrir. Não se trata de substituir tudo que você já construiu. Se trata de entender onde Rust entrega valor real e como integrar essa ferramenta ao seu arsenal.
Neste guia, vou mostrar como usar Rust no domínio de backend de forma prática. Vamos partir do "por quê" até chegar em código funcionando, com exemplos que você pode adaptar ao seu contexto. Se você já tem pelo menos dois anos de experiência com desenvolvimento web, vai se sentir em casa.
Se você quer se aprofundar no lado Node.js e TypeScript da equação, recomendo a leitura do nosso Guia Definitivo de Engenharia Backend com Node.js e TypeScript.
Por que usar Rust no Backend
Desempenho sem surpresas
Node.js usa o V8 com garbage collector. Na maior parte do tempo isso é invisível. Mas quando você tem milhares de conexões simultâneas, aquele "pause" do GC aparece como picos de latência no P99. Rust não tem garbage collector. A memória é gerenciada em tempo de compilação pelo sistema de ownership. Isso significa que a latência é consistente e previsível, requisição após requisição.
Segurança de memória em tempo de compilação
Rust elimina classes inteiras de bugs antes do código executar: null pointer dereference, data races, use-after-free. O compilador é rigoroso (às vezes brutalmente rigoroso), mas cada erro que ele te força a corrigir é um bug que nunca vai chegar à produção.
Consumo de recursos drasticamente menor
Um serviço equivalente em Rust costuma consumir entre 5x e 20x menos memória que a versão em Node.js. Em cenários de Kubernetes com dezenas de réplicas, isso se traduz diretamente em economia de infraestrutura.
Casos de uso reais onde Rust brilha no backend
Rust não é a melhor escolha para todo tipo de backend. Ele brilha em cenários específicos:
- Serviços de alta performance: APIs que precisam processar milhões de requisições com latência mínima. A Cloudflare usa Rust extensivamente no seu edge computing.
- Processamento de dados em tempo real: pipelines de ingestão, transformação e roteamento de eventos. A Discord migrou serviços críticos de Go para Rust e reduziu a latência de tail em 10x.
- Infraestrutura e ferramentas de plataforma: proxies reversos, load balancers, e serviços de autenticação que ficam no caminho crítico de toda requisição.
- Sistemas financeiros: onde previsibilidade de latência e correção do código são requisitos regulatórios, não apenas desejáveis.
Setup: Configurando um Projeto Rust para Backend
Escolhendo o framework web
O ecossistema Rust para backend amadureceu significativamente. As principais opções são:
- Axum: mantido pelo time do Tokio, é a escolha mais moderna. Integra naturalmente com o ecossistema Tokio e tem ergonomia excelente.
- Actix-web: um dos mais rápidos em benchmarks. Tem uma comunidade grande e bastante documentação.
- Poem: foco em simplicidade e ergonomia. Boa escolha para quem está começando com Rust.
Para este guia, vamos usar Axum por ser o que melhor representa a direção do ecossistema.
Criando o projeto
Primeiro, instale o Rust via rustup (o gerenciador oficial de toolchains):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shAgora crie o projeto:
cargo new meu-servico-backend
cd meu-servico-backendAdicione as dependências no Cargo.toml:
[package]
name = "meu-servico-backend"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = "0.3"Exemplo Prático: API REST com Axum
Vamos construir uma API de gerenciamento de tarefas. Primeiro, o código em Rust. Depois, vou mostrar como um cliente TypeScript consome essa API, para que a ponte entre os dois mundos fique clara.
Estrutura do servidor Rust
use axum::{
extract::{Path, State},
http::StatusCode,
routing::{get, post, put},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Clone)]
struct Task {
id: u64,
title: String,
description: String,
completed: bool,
}
#[derive(Debug, Deserialize)]
struct CreateTaskRequest {
title: String,
description: String,
}
type AppState = Arc<RwLock<HashMap<u64, Task>>>;
#[tokio::main]
async fn main() {
tracing_subscriber::init();
let state: AppState = Arc::new(RwLock::new(HashMap::new()));
let app = Router::new()
.route("/tasks", get(list_tasks).post(create_task))
.route("/tasks/:id", get(get_task).put(update_task))
.route("/health", get(health_check))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
tracing::info!("Servidor rodando na porta 3000");
axum::serve(listener, app).await.unwrap();
}
async fn health_check() -> &'static str {
"OK"
}
async fn list_tasks(
State(state): State<AppState>,
) -> Json<Vec<Task>> {
let tasks = state.read().await;
let task_list: Vec<Task> = tasks.values().cloned().collect();
Json(task_list)
}
async fn create_task(
State(state): State<AppState>,
Json(payload): Json<CreateTaskRequest>,
) -> (StatusCode, Json<Task>) {
let mut tasks = state.write().await;
let id = (tasks.len() as u64) + 1;
let task = Task {
id,
title: payload.title,
description: payload.description,
completed: false,
};
tasks.insert(id, task.clone());
(StatusCode::CREATED, Json(task))
}
async fn get_task(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> Result<Json<Task>, StatusCode> {
let tasks = state.read().await;
tasks
.get(&id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
async fn update_task(
State(state): State<AppState>,
Path(id): Path<u64>,
Json(payload): Json<CreateTaskRequest>,
) -> Result<Json<Task>, StatusCode> {
let mut tasks = state.write().await;
match tasks.get_mut(&id) {
Some(task) => {
task.title = payload.title;
task.description = payload.description;
Ok(Json(task.clone()))
}
None => Err(StatusCode::NOT_FOUND),
}
}Note como o sistema de tipos de Rust já garante que o corpo da requisição será validado automaticamente pelo Json<CreateTaskRequest>. Se o payload não corresponder à struct, o Axum retorna 422 sem que você precise escrever uma única linha de validação.
Cliente TypeScript consumindo a API Rust
Agora, o lado TypeScript. Esse é o tipo de integração mais comum: seu frontend ou outro microsserviço em Node.js consumindo a API Rust.
interface Task {
id: number;
title: string;
description: string;
completed: boolean;
}
interface CreateTaskRequest {
title: string;
description: string;
}
class TaskApiClient {
private baseUrl: string;
constructor(baseUrl: string = "http://localhost:3000") {
this.baseUrl = baseUrl;
}
async listTasks(): Promise<Task[]> {
const response = await fetch(`${this.baseUrl}/tasks`);
if (!response.ok) {
throw new Error(`Falha ao listar tarefas: ${response.status}`);
}
return response.json();
}
async createTask(data: CreateTaskRequest): Promise<Task> {
const response = await fetch(`${this.baseUrl}/tasks`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!response.ok) {
throw new Error(`Falha ao criar tarefa: ${response.status}`);
}
return response.json();
}
async getTask(id: number): Promise<Task | null> {
const response = await fetch(`${this.baseUrl}/tasks/${id}`);
if (response.status === 404) {
return null;
}
if (!response.ok) {
throw new Error(`Falha ao buscar tarefa: ${response.status}`);
}
return response.json();
}
}
async function main(): Promise<void> {
const client = new TaskApiClient();
const newTask = await client.createTask({
title: "Aprender Rust para backend",
description: "Seguir o guia do Vivo de Codigo",
});
console.log("Tarefa criada:", newTask);
const tasks = await client.listTasks();
console.log(`Total de tarefas: ${tasks.length}`);
const fetched = await client.getTask(newTask.id);
console.log("Tarefa recuperada:", fetched);
}
main().catch(console.error);Segundo exemplo: middleware de logging e métricas em TypeScript
Em arquiteturas onde o Rust serve como serviço de alta performance e o Node.js orquestra, é comum ter um layer intermediário em TypeScript que adiciona observabilidade:
import { performance } from "node:perf_hooks";
interface RequestMetrics {
endpoint: string;
method: string;
statusCode: number;
durationMs: number;
timestamp: Date;
}
class InstrumentedApiClient {
private baseUrl: string;
private metrics: RequestMetrics[] = [];
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private async instrumentedFetch(
path: string,
options: RequestInit = {}
): Promise<Response> {
const start = performance.now();
const method = options.method ?? "GET";
try {
const response = await fetch(`${this.baseUrl}${path}`, {
...options,
headers: {
"Content-Type": "application/json",
"X-Request-Id": crypto.randomUUID(),
...options.headers,
},
});
const duration = performance.now() - start;
const metric: RequestMetrics = {
endpoint: path,
method,
statusCode: response.status,
durationMs: Math.round(duration * 100) / 100,
timestamp: new Date(),
};
this.metrics.push(metric);
console.log(
`[${method}] ${path} -> ${response.status} (${metric.durationMs}ms)`
);
if (duration > 100) {
console.warn(
`Alerta: requisicao lenta detectada para ${path}: ${metric.durationMs}ms`
);
}
return response;
} catch (error) {
const duration = performance.now() - start;
this.metrics.push({
endpoint: path,
method,
statusCode: 0,
durationMs: Math.round(duration * 100) / 100,
timestamp: new Date(),
});
throw error;
}
}
async createTask(title: string, description: string): Promise<unknown> {
const response = await this.instrumentedFetch("/tasks", {
method: "POST",
body: JSON.stringify({ title, description }),
});
return response.json();
}
async listTasks(): Promise<unknown> {
const response = await this.instrumentedFetch("/tasks");
return response.json();
}
getAverageLatency(): number {
if (this.metrics.length === 0) return 0;
const total = this.metrics.reduce((sum, m) => sum + m.durationMs, 0);
return Math.round((total / this.metrics.length) * 100) / 100;
}
getMetricsSummary(): string {
const avg = this.getAverageLatency();
const total = this.metrics.length;
const errors = this.metrics.filter(
(m) => m.statusCode >= 400 || m.statusCode === 0
).length;
return `Total: ${total} requisicoes | Latencia media: ${avg}ms | Erros: ${errors}`;
}
}Esse padrão é extremamente útil quando seu backend Rust é o motor de alta performance e o Node.js cuida de orquestração, BFF (Backend for Frontend) e observabilidade.
Boas Práticas para Rust no Backend
Arquitetura em camadas
Assim como no TypeScript, organize seu código Rust em camadas bem definidas:
- Handlers (equivalente aos Controllers): recebem a requisição, delegam para a camada de serviço.
- Services: contêm a lógica de negócio. Não conhecem HTTP.
- Repositories: acessam o banco de dados. Use SQLx para queries type-safe em tempo de compilação.
Tratamento de erros com tipos
Evite unwrap() em código de produção. Crie seus próprios tipos de erro e use o operador ? para propagação limpa. O padrão mais comum é criar um AppError que implementa IntoResponse do Axum, mapeando cada variante para o status HTTP correto.
Testes integrados desde o início
O Axum permite criar testes de integração sem subir um servidor real, usando tower::ServiceExt. Isso torna os testes rápidos e confiáveis. Escreva testes para cada endpoint desde o primeiro dia.
Observabilidade com tracing
O ecossistema tracing do Rust é maduro e se integra com OpenTelemetry. Configure spans estruturados nos handlers para ter visibilidade completa em produção
Guias Relacionados
Explore mais guias técnicos
Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.
Ver todos os guias