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_asreduce 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_LOGyenv_loggerpara depuración.
Errores comunes y cómo evitarlos
- No verificar el esquema al compilar: usa
DATABASE_URLdurante desarrollo para quesqlxverifique 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.
¿Quieres comentar?
Inicia sesión con Telegram para participar en la conversación