Cómo construir una API REST en Rust con Actix-web y SQLx paso a paso

rust Cómo construir una API REST en Rust con Actix-web y SQLx paso a paso

Cómo construir una API REST en Rust con Actix-web y SQLx paso a paso

Este tutorial te lleva desde el esqueleto hasta una API REST mínima con Actix-web, SQLx (Postgres), migraciones y Docker. Objetivos: seguridad básica, manejo de errores, conexión pool y consultas tipeadas en tiempo de compilación.

¿Por qué estas herramientas?

  • Actix-web: alto rendimiento, middleware flexible.
  • SQLx: queries verificadas en compile-time (con feature "offline" o conexión a DB), async y soporte para Postgres.
  • Docker: entorno reproducible.

Estructura de carpetas

my_api/
├─ Dockerfile
├─ docker-compose.yml
├─ .env
├─ Cargo.toml
├─ migrations/           # sqlx / refinery migrations
└─ src/
   ├─ main.rs
   ├─ db.rs
   ├─ handlers.rs
   ├─ models.rs
   └─ error.rs

Cargo.toml (dependencias principales)

[package]
name = "my_api"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sqlx = { version = "0.6", features = ["runtime-tokio-native-tls", "postgres", "macros"] }
env_logger = "0.10"
dotenvy = "0.15"
thiserror = "1.0"
log = "0.4"

Variables de entorno (.env)

DATABASE_URL=postgres://postgres:password@db:5432/my_api
RUST_LOG=info

Docker Compose (postgres + app)

version: '3.8'
services:
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: my_api
    ports:
      - "5432:5432"
    volumes:
      - db-data:/var/lib/postgresql/data

  app:
    build: .
    env_file: .env
    depends_on:
      - db
    ports:
      - "8080:8080"

volumes:
  db-data:

Archivo principal: src/main.rs

use actix_web::{web, App, HttpServer};
use dotenvy::dotenv;
use std::env;

mod db;
mod handlers;
mod models;
mod error;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    env_logger::init();
    let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");

    let pool = db::init_pool(&database_url).await.expect("DB pool");

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

Conexion a DB: src/db.rs

use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;

pub async fn init_pool(database_url: &str) -> Result {
    PgPoolOptions::new()
        .max_connections(5)
        .connect(database_url)
        .await
}

Modelos y DTOs: src/models.rs

use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
pub struct CreateUser {
    pub name: String,
    pub email: String,
}

#[derive(Serialize, Deserialize, sqlx::FromRow, Debug)]
pub struct User {
    pub id: i32,
    pub name: String,
    pub email: String,
}

Manejadores HTTP: src/handlers.rs

use actix_web::{web, HttpResponse};
use sqlx::PgPool;
use crate::models::{CreateUser, User};

pub async fn create_user(pool: web::Data, payload: web::Json) -> HttpResponse {
    let query = r#"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email"#;

    match sqlx::query_as::<_, User>(query)
        .bind(&payload.name)
        .bind(&payload.email)
        .fetch_one(pool.get_ref())
        .await
    {
        Ok(user) => HttpResponse::Ok().json(user),
        Err(e) => {
            log::error!("DB error: {}", e);
            HttpResponse::InternalServerError().body("Internal error")
        }
    }
}

pub async fn get_user(pool: web::Data, path: web::Path) -> HttpResponse {
    let id = path.into_inner();
    let query = r#"SELECT id, name, email FROM users WHERE id = $1"#;

    match sqlx::query_as::<_, User>(query).bind(id).fetch_one(pool.get_ref()).await {
        Ok(user) => HttpResponse::Ok().json(user),
        Err(sqlx::Error::RowNotFound) => HttpResponse::NotFound().body("Not found"),
        Err(e) => {
            log::error!("DB error: {}", e);
            HttpResponse::InternalServerError().body("Internal error")
        }
    }
}

Migraciones (ejemplo SQL)

-- migrations/0001_create_users.sql
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(200) NOT NULL UNIQUE
);

Usa sqlx-cli o herramientas como refinery para ejecutar migraciones en Docker. Ejemplo con sqlx-cli (local):

cargo install sqlx-cli --no-default-features --features postgres
export DATABASE_URL=postgres://postgres:password@localhost:5432/my_api
sqlx migrate run

Pruebas rápidas

Inicia Docker Compose y prueba:

docker compose up --build

# POST
curl -X POST -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com"}' \
  http://localhost:8080/users

# GET
curl http://localhost:8080/users/1

Buenas prácticas y por qué

  • Pool de conexiones: evita crear conexiones por petición; usa PgPool.
  • Queries tipeadas: sqlx::query_as reduce errores en producción.
  • Separación: handlers no deben contener lógica de negocio compleja; crea servicios si crece la app.
  • Migraciones versionadas: evita DDL manual en producción.
  • Logging: usa RUST_LOG y env_logger para depuración.

Errores comunes y cómo evitarlos

  • No verificar el esquema al compilar: usa DATABASE_URL durante desarrollo para que sqlx verifique queries o usa el modo offline correctamente.
  • Bloquear el runtime: evita operaciones sincrónicas pesadas en handlers.
  • No sanear inputs: aunque SQLx usa bindings, valida formatos y tamaños en DTOs.

Siguiente paso

Para producción añade autenticación (JWT), límites de tasa, pruebas de integración y un observability stack (metrics + tracing). Un consejo avanzado: habilita las verificaciones en tiempo de compilación de SQLx ejecutando cargo sqlx prepare -- --lib durante tu CI con una DB temporal para atrapar regressiones de esquema antes del deploy.

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