Introdução
O Rust combina desempenho com garantias de segurança, o que o torna excelente para criar APIs web de alto desempenho. Com frameworks como Actix-web e Axum, o Rust proporciona um desenvolvimento web ergonómico, mantendo abstrações de custo zero. Este guia aborda a criação de APIs web com Rust prontas para produção.
Configuração do Projeto
Criar um Novo Projeto
cargo new api-server
cd api-server
Dependências (Cargo.toml)
[package]
name = "api-server"
version = "0.1.0"
edition = "2021"
[dependencies]
# Framework web
axum = "0.7"
tokio = { version = "1", features = ["full"] }
# Serialização
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Base de dados
sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
# Utilitários
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
tower-http = { version = "0.5", features = ["cors", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Configuração
dotenvy = "0.15"
# Tratamento de erros
thiserror = "1"
anyhow = "1"
Servidor Básico com Axum
Exemplo Mínimo
// src/main.rs
use axum::{routing::get, Router};
use std::net::SocketAddr;
#[tokio::main]
async fn main() {
// Inicializar o tracing
tracing_subscriber::fmt::init();
// Construir o router
let app = Router::new()
.route("/", get(root))
.route("/health", get(health));
// Iniciar o servidor
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
tracing::info!("Listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn root() -> &'static str {
"Hello, World!"
}
async fn health() -> &'static str {
"OK"
}
Tratamento de Pedidos
Parâmetros de Caminho
use axum::{extract::Path, routing::get, Router};
async fn get_user(Path(user_id): Path<i32>) -> String {
format!("User ID: {}", user_id)
}
async fn get_post(Path((user_id, post_id)): Path<(i32, i32)>) -> String {
format!("User: {}, Post: {}", user_id, post_id)
}
let app = Router::new()
.route("/users/:user_id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_post));
Parâmetros de Query
use axum::{extract::Query, routing::get};
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
per_page: Option<u32>,
}
async fn list_items(Query(params): Query<Pagination>) -> String {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(20);
format!("Page: {}, Per page: {}", page, per_page)
}
Corpo do Pedido em JSON
use axum::{extract::Json, routing::post};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: i32,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
let user = User {
id: 1,
name: payload.name,
email: payload.email,
};
Json(user)
}
let app = Router::new()
.route("/users", post(create_user));
Integração com Base de Dados
Configuração do Pool de Ligações
use sqlx::postgres::PgPoolOptions;
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
db: sqlx::PgPool,
}
#[tokio::main]
async fn main() {
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL")
.expect("DATABASE_URL must be set");
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(&database_url)
.await
.expect("Failed to create pool");
// Executar migrações
sqlx::migrate!("./migrations")
.run(&pool)
.await
.expect("Failed to run migrations");
let state = AppState { db: pool };
let app = Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user))
.with_state(state);
// ... iniciar o servidor
}
Operações CRUD
use axum::{
extract::{Path, State, Json},
http::StatusCode,
response::IntoResponse,
};
use sqlx::FromRow;
#[derive(Serialize, FromRow)]
struct User {
id: i32,
name: String,
email: String,
created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize)]
struct CreateUserRequest {
name: String,
email: String,
}
async fn list_users(
State(state): State<AppState>,
) -> Result<Json<Vec<User>>, StatusCode> {
let users = sqlx::query_as::<_, User>("SELECT * FROM users ORDER BY id")
.fetch_all(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(users))
}
async fn get_user(
State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<Json<User>, StatusCode> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(user))
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUserRequest>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
let user = sqlx::query_as::<_, User>(
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING *"
)
.bind(&payload.name)
.bind(&payload.email)
.fetch_one(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok((StatusCode::CREATED, Json(user)))
}
async fn update_user(
State(state): State<AppState>,
Path(id): Path<i32>,
Json(payload): Json<CreateUserRequest>,
) -> Result<Json<User>, StatusCode> {
let user = sqlx::query_as::<_, User>(
"UPDATE users SET name = $1, email = $2 WHERE id = $3 RETURNING *"
)
.bind(&payload.name)
.bind(&payload.email)
.bind(id)
.fetch_optional(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
Ok(Json(user))
}
async fn delete_user(
State(state): State<AppState>,
Path(id): Path<i32>,
) -> StatusCode {
let result = sqlx::query("DELETE FROM users WHERE id = $1")
.bind(id)
.execute(&state.db)
.await;
match result {
Ok(r) if r.rows_affected() > 0 => StatusCode::NO_CONTENT,
Ok(_) => StatusCode::NOT_FOUND,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
Tratamento de Erros
Tipo de Erro Personalizado
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Resource not found")]
NotFound,
#[error("Validation error: {0}")]
Validation(String),
#[error("Database error")]
Database(#[from] sqlx::Error),
#[error("Internal server error")]
Internal(#[from] anyhow::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, message) = match &self {
AppError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
AppError::Validation(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
AppError::Database(e) => {
tracing::error!("Database error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string())
}
AppError::Internal(e) => {
tracing::error!("Internal error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Internal error".to_string())
}
};
let body = Json(json!({
"error": message
}));
(status, body).into_response()
}
}
// Usar nos handlers
async fn get_user(
State(state): State<AppState>,
Path(id): Path<i32>,
) -> Result<Json<User>, AppError> {
let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id)
.fetch_optional(&state.db)
.await?
.ok_or(AppError::NotFound)?;
Ok(Json(user))
}
Middleware
Logging e Tracing
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
))
.with(tracing_subscriber::fmt::layer())
.init();
let app = Router::new()
.route("/users", get(list_users))
.layer(TraceLayer::new_for_http());
// ...
}
CORS
use tower_http::cors::{Any, CorsLayer};
let cors = CorsLayer::new()
.allow_methods(Any)
.allow_origin(Any)
.allow_headers(Any);
let app = Router::new()
.route("/users", get(list_users))
.layer(cors);
Middleware de Autenticação
use axum::{
extract::Request,
http::{header, StatusCode},
middleware::{self, Next},
response::Response,
};
async fn auth_middleware(
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|h| h.to_str().ok());
match auth_header {
Some(token) if token.starts_with("Bearer ") => {
// Validar o token aqui
let token = &token[7..];
if validate_token(token) {
Ok(next.run(request).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
fn validate_token(token: &str) -> bool {
// Lógica de validação JWT
true
}
// Aplicar às rotas
let protected_routes = Router::new()
.route("/users", get(list_users))
.layer(middleware::from_fn(auth_middleware));
let public_routes = Router::new()
.route("/health", get(health));
let app = Router::new()
.merge(public_routes)
.merge(protected_routes);
Testes
Testes de Integração
// tests/api_tests.rs
use axum::{
body::Body,
http::{Request, StatusCode},
};
use tower::ServiceExt;
#[tokio::test]
async fn test_health() {
let app = create_app().await; // O builder da sua app
let response = app
.oneshot(Request::builder().uri("/health").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn test_create_user() {
let app = create_app().await;
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/users")
.header("content-type", "application/json")
.body(Body::from(r#"{"name":"Test","email":"[email protected]"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
}
Implementação em Produção
Dockerfile
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/api-server /usr/local/bin/
EXPOSE 3000
CMD ["api-server"]
Variáveis de Ambiente
let config = Config {
database_url: std::env::var("DATABASE_URL").expect("DATABASE_URL required"),
port: std::env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse()
.expect("PORT must be a number"),
jwt_secret: std::env::var("JWT_SECRET").expect("JWT_SECRET required"),
};
Conclusão
O Rust oferece um excelente desempenho para APIs web com frameworks como Axum. O sistema de tipos deteta erros em tempo de compilação, e o async/await permite um tratamento eficiente de pedidos concorrentes. Embora a curva de aprendizagem seja mais acentuada do que em linguagens dinâmicas, o resultado são APIs robustas, rápidas e fáceis de manter.