API REST en Rust con Actix-web y SQLx: diseño práctico, errores y despliegue

rust API REST en Rust con Actix-web y SQLx: diseño práctico, errores y despliegue

API REST en Rust con Actix-web y SQLx: diseño práctico, errores y despliegue

En este artículo verás cómo construir una API REST sencilla y robusta en Rust usando Actix-web y SQLx (Postgres). Enfócate en: inicialización del pool, rutas y handlers asincrónicos, manejo de errores, transacciones, cierre ordenado y despliegue en Docker.

1) Dependencias y estructura

Cargo.toml mínimo (resalta características importantes):

[package]
name = "api-rust"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "uuid"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tracing = "0.1"
tracing-subscriber = "0.3"
uuid = { version = "1", features = ["serde", "v4"] }

2) Configuración del pool y logging

Crea el Pool de Postgres con configuraciones razonables y habilita tracing para logs estructurados:

use sqlx::postgres::PgPoolOptions;
use tracing_subscriber::fmt::init as tracing_init;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    tracing_init();

    let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL required");
    let pool = PgPoolOptions::new()
        .max_connections(10)
        .connect(&db_url)
        .await
        .expect("failed to connect to Postgres");

    // build and run server (ver siguiente sección)
    Ok(())
}

3) Modelos y handlers

Ejemplo simple: usuario con id (UUID) y nombre.

use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(Serialize, Deserialize, sqlx::FromRow)]
struct User {
    id: Uuid,
    name: String,
}

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

Handler para crear usuario (usa transacción para ejemplo):

use actix_web::{web, HttpResponse, Responder};

async fn create_user(
    pool: web::Data<:pgpool>,
    payload: web::Json,
) -> actix_web::Result {
    let mut tx = pool.begin().await.map_err(|e| {
        actix_web::error::ErrorInternalServerError(format!("DB TX start: {}", e))
    })?;

    let id = Uuid::new_v4();
    sqlx::query!("INSERT INTO users (id, name) VALUES ($1, $2)", id, payload.name)
        .execute(&mut tx)
        .await
        .map_err(|e| actix_web::error::ErrorInternalServerError(format!("Insert: {}", e)))?;

    tx.commit().await.map_err(|e| {
        actix_web::error::ErrorInternalServerError(format!("TX commit: {}", e))
    })?;

    Ok(HttpResponse::Created().json(User { id, name: payload.name.clone() }))
}

Handler para obtener usuario por id:

async fn get_user(
    pool: web::Data<:pgpool>,
    path: web::Path,
) -> actix_web::Result {
    let id = path.into_inner();
    let user = sqlx::query_as::<_, User>("SELECT id, name FROM users WHERE id = $1")
        .bind(id)
        .fetch_one(pool.get_ref())
        .await
        .map_err(|e| actix_web::error::ErrorNotFound(format!("not found: {}", e)))?;

    Ok(HttpResponse::Ok().json(user))
}

4) Rutas, middleware y servidor

use actix_web::{App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    tracing_init();

    let db_url = std::env::var("DATABASE_URL").expect("DATABASE_URL required");
    let pool = PgPoolOptions::new().max_connections(10).connect(&db_url).await.unwrap();

    let server = HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(pool.clone()))
            .route("/users", web::post().to(create_user))
            .route("/users/{id}", web::get().to(get_user))
    })
    .bind(("0.0.0.0", 8080))?
    .run();

    // graceful shutdown on Ctrl+C
    let handle = server.handle();
    tokio::spawn(async move {
        tokio::signal::ctrl_c().await.unwrap();
        tracing::info!("shutting down server");
        handle.stop(true).await;
    });

    server.await
}

5) Manejo de errores bien estructurado

No devuelvas 500 por defecto: implementa un tipo de error que traduzca a códigos HTTP y mensajes no verbosos para el usuario.

use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("not found")]
    NotFound,
    #[error("bad request: {0}")]
    BadRequest(String),
    #[error("internal")]
    Internal(#[from] anyhow::Error),
}

impl ResponseError for AppError {
    fn status_code(&self) -> StatusCode {
        match self {
            AppError::NotFound => StatusCode::NOT_FOUND,
            AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
            AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }

    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code()).json(serde_json::json!({
            "error": self.to_string()
        }))
    }
}

6) Migraciones y SQLx

Usa sqlx-cli o cualquier migrator. Ejemplo con sqlx-cli:

cargo install sqlx-cli --no-default-features --features postgres

# crear carpeta migrations y agregar SQL
sqlx migrate add create_users
# luego ejecutar
sqlx migrate run

7) Dockerfile — multi-stage para release

FROM --platform=linux/amd64 rust:1.70 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
COPY --from=builder /app/target/release/api-rust /usr/local/bin/api-rust
ENV RUST_LOG=info
EXPOSE 8080
CMD ["/usr/local/bin/api-rust"]

8) Buenas prácticas y rendimiento

  • Configura pool.max_connections según la capacidad de Postgres y los límites del entorno.
  • Usa prepared statements (sqlx query! macros cuando puedas) para seguridad y rendimiento.
  • Instrumenta con tracing + Prometheus/OpenTelemetry para observar latencias y errores.
  • Haz health checks y endpoints liveness/readiness para orquestadores.

9) Seguridad y validaciones

Valida y sanitiza entradas. Usa TLS en transporte y en la conexión a la base de datos. Evita exponer mensajes de error internos al cliente. Nunca concatenes SQL dinámico sin parámetros.

Código de ejemplo usa bind y query! para evitar inyecciones; verifica siempre que los parámetros estén tipados y limitados.

Si necesitas pruebas, escribe tests de integración lanzando una instancia ephemeral de Postgres (testcontainers) y ejecuta migraciones antes de correr los tests.

Siguiente paso: integra tracing y métricas, añade circuit breaker y rate limiting. Un consejo avanzado: afina el tamaño del pool y el número de hilos tokio (RUNTIME) respecto al tipo de trabajo (I/O vs CPU). Para cargas intensas en CPU considera separar tareas pesadas a workers y mantener el runtime web optimizado para I/O.

Comentarios
¿Quieres comentar?

Inicia sesión con Telegram para participar en la conversación


Comentarios (0)

Aún no hay comentarios. ¡Sé el primero en comentar!

Iniciar Sesión