API REST en Rust con MongoDB, Bcrypt, JWT y Swagger

Tabla de Contenidos

1. Introducción

Este manual detalla cómo construir una API RESTful segura en Rust que incluye:

Requisitos previos:

2. Configuración del Proyecto

2.1 Crear un nuevo proyecto

cargo new rust-mongodb-api --bin
cd rust-mongodb-api

2.2 Configurar dependencias en Cargo.toml

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

[dependencies]
actix-web = "4.0"                # Framework web
actix-cors = "0.6"               # Manejo de CORS
mongodb = { version = "2.0", features = ["sync"] } # Driver MongoDB
bcrypt = "0.13"                  # Hashing de contraseñas
jsonwebtoken = "8.0"             # JWT
chrono = { version = "0.4", features = ["serde"] } # Manejo de fechas
dotenv = "0.15"                  # Variables de entorno
serde = { version = "1.0", features = ["derive"] } # Serialización
serde_json = "1.0"               # Manejo de JSON
thiserror = "1.0"                # Manejo de errores
validator = { version = "1.0", features = ["derive"] } # Validación
utoipa = { version = "1.0", features = ["actix_extras"] } # Swagger
utoipa-swagger-ui = "1.0"        # UI para Swagger

2.3 Configurar variables de entorno (.env)

DATABASE_URL=mongodb://localhost:27017
DATABASE_NAME=rust_api
JWT_SECRET=tu_super_secreto_jwt
JWT_EXPIRATION_HOURS=24
BCRYPT_COST=12
SERVER_ADDRESS=127.0.0.1:8080

3. Estructura del Proyecto

rust-mongodb-api/
├── src/
│   ├── main.rs                # Punto de entrada
│   ├── models/                # Modelos de datos
│   │   ├── user.rs            # Modelo de usuario
│   │   └── mod.rs             # Exportación de modelos
│   ├── handlers/              # Manejadores de rutas
│   │   ├── auth.rs            # Autenticación
│   │   └── mod.rs             # Exportación de handlers
│   ├── db/                    # Conexión a MongoDB
│   │   ├── connection.rs      # Manejo de conexión
│   │   └── mod.rs             # Exportación de db
│   ├── utils/                 # Utilidades
│   │   ├── auth.rs            # Utilidades de autenticación
│   │   └── mod.rs             # Exportación de utils
│   ├── errors.rs              # Manejo de errores personalizados
│   └── app_state.rs           # Estado compartido de la aplicación
├── .env                       # Variables de entorno
└── Cargo.toml                 # Configuración del proyecto

4. Modelos de Datos con MongoDB

4.1 Modelo de Usuario

// src/models/user.rs
use mongodb::bson::oid::ObjectId;
use serde::{Deserialize, Serialize};
use validator::Validate;

#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct User {
    #[serde(rename = "_id", skip_serializing_if = "Option::is_none")]
    pub id: Option<ObjectId>,
    #[validate(length(min = 3, max = 50))]
    pub username: String,
    #[validate(email)]
    pub email: String,
    pub password: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
    pub updated_at: chrono::DateTime<chrono::Utc>,
}

#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct RegisterUser {
    #[validate(length(min = 3, max = 50))]
    pub username: String,
    #[validate(email)]
    pub email: String,
    #[validate(length(min = 8))]
    pub password: String,
}

#[derive(Debug, Serialize, Deserialize, Validate)]
pub struct LoginUser {
    #[validate(email)]
    pub email: String,
    pub password: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct UserResponse {
    pub id: String,
    pub username: String,
    pub email: String,
    pub created_at: chrono::DateTime<chrono::Utc>,
}

4.2 Implementación de conversiones

// src/models/user.rs
impl From<User> for UserResponse {
    fn from(user: User) -> Self {
        UserResponse {
            id: user.id.unwrap().to_hex(),
            username: user.username,
            email: user.email,
            created_at: user.created_at,
        }
    }
}

5. Autenticación con Bcrypt y JWT

5.1 Utilidades de autenticación

// src/utils/auth.rs
use bcrypt::{hash, verify, DEFAULT_COST};
use chrono::{Duration, Utc};
use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
use std::env;

#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
    pub sub: String, // ID del usuario
    pub exp: usize,  // Tiempo de expiración
    pub iat: usize,  // Tiempo de emisión
}

#[derive(Debug, Serialize, Deserialize)]
pub struct TokenResponse {
    pub token: String,
    pub expires_in: usize,
}

// Hashear contraseña
pub fn hash_password(password: &str) -> Result<String, bcrypt::BcryptError> {
    let cost = env::var("BCRYPT_COST")
        .unwrap_or_else(|_| "12".to_string())
        .parse()
        .unwrap_or(DEFAULT_COST);
    
    hash(password, cost)
}

// Verificar contraseña
pub fn verify_password(password: &str, hashed: &str) -> Result<bool, bcrypt::BcryptError> {
    verify(password, hashed)
}

// Generar JWT
pub fn create_jwt(user_id: &str) -> Result<TokenResponse, jsonwebtoken::errors::Error> {
    let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
    let expiration_hours = env::var("JWT_EXPIRATION_HOURS")
        .unwrap_or_else(|_| "24".to_string())
        .parse()
        .unwrap_or(24);
    
    let now = Utc::now();
    let iat = now.timestamp() as usize;
    let exp = (now + Duration::hours(expiration_hours)).timestamp() as usize;
    
    let claims = Claims {
        sub: user_id.to_string(),
        exp,
        iat,
    };
    
    let token = encode(
        &Header::default(),
        &claims,
        &EncodingKey::from_secret(secret.as_ref()),
    )?;
    
    Ok(TokenResponse {
        token,
        expires_in: expiration_hours * 3600,
    })
}

// Validar JWT
pub fn validate_jwt(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
    
    let decoded = decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_ref()),
        &Validation::new(Algorithm::HS256),
    )?;
    
    Ok(decoded.claims)
}

5.2 Manejador de autenticación

// src/handlers/auth.rs
use actix_web::{post, web, HttpResponse, Responder};
use mongodb::bson::doc;
use serde::{Deserialize, Serialize};
use validator::Validate;

use crate::{
    app_state::AppState,
    errors::ServiceError,
    models::user::{LoginUser, RegisterUser, User, UserResponse},
    utils::auth::{create_jwt, verify_password},
};

#[post("/register")]
pub async fn register(
    data: web::Json<RegisterUser>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ServiceError> {
    // Validar datos de entrada
    data.validate()?;
    
    // Verificar si el usuario ya existe
    let existing_user = state
        .db
        .collection::<User>("users")
        .find_one(doc! { "email": &data.email }, None)
        .await
        .map_err(|_| ServiceError::InternalServerError)?;
    
    if existing_user.is_some() {
        return Err(ServiceError::Conflict("El email ya está registrado".into()));
    }
    
    // Hashear la contraseña
    let hashed_password = hash_password(&data.password)?;
    
    // Crear nuevo usuario
    let new_user = User {
        id: None,
        username: data.username.clone(),
        email: data.email.clone(),
        password: hashed_password,
        created_at: chrono::Utc::now(),
        updated_at: chrono::Utc::now(),
    };
    
    // Insertar en la base de datos
    let result = state
        .db
        .collection::<User>("users")
        .insert_one(new_user, None)
        .await
        .map_err(|_| ServiceError::InternalServerError)?;
    
    Ok(HttpResponse::Created().json({
        #[derive(Serialize)]
        struct Response {
            id: String,
        }
        Response {
            id: result.inserted_id.as_object_id().unwrap().to_hex(),
        }
    }))
}

#[post("/login")]
pub async fn login(
    data: web::Json<LoginUser>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ServiceError> {
    // Validar datos de entrada
    data.validate()?;
    
    // Buscar usuario por email
    let user = state
        .db
        .collection::<User>("users")
        .find_one(doc! { "email": &data.email }, None)
        .await
        .map_err(|_| ServiceError::InternalServerError)?
        .ok_or(ServiceError::Unauthorized)?;
    
    // Verificar contraseña
    if !verify_password(&data.password, &user.password)? {
        return Err(ServiceError::Unauthorized);
    }
    
    // Generar JWT
    let token = create_jwt(&user.id.unwrap().to_hex())?;
    
    Ok(HttpResponse::Ok().json(token))
}

6. Definición de Rutas

6.1 Configuración principal

// src/main.rs
mod app_state;
mod db;
mod errors;
mod handlers;
mod models;
mod utils;

use actix_web::{web, App, HttpServer};
use dotenv::dotenv;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    dotenv().ok();
    
    // Configurar MongoDB
    let db = db::connect().await.expect("Error al conectar con MongoDB");
    let app_state = web::Data::new(app_state::AppState { db });
    
    // Configurar Swagger
    let openapi = ApiDoc::openapi();
    
    // Iniciar servidor
    HttpServer::new(move || {
        App::new()
            .app_data(app_state.clone())
            .service(
                SwaggerUi::new("/swagger-ui/{_:.*}")
                    .url("/api-docs/openapi.json", openapi.clone()),
            )
            .configure(handlers::config)
    })
    .bind(std::env::var("SERVER_ADDRESS").unwrap_or("127.0.0.1:8080".into()))?
    .run()
    .await
}

#[derive(OpenApi)]
#[openapi(
    paths(
        handlers::auth::register,
        handlers::auth::login,
    ),
    components(
        schemas(
            models::user::RegisterUser,
            models::user::LoginUser,
            models::user::UserResponse,
            utils::auth::TokenResponse,
            errors::ServiceError,
        )
    ),
    tags(
        (name = "auth", description = "Autenticación de usuarios"),
    ),
)]
struct ApiDoc;

6.2 Configuración de handlers

// src/handlers/mod.rs
use actix_web::web;

pub mod auth;

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api/v1")
            .service(
                web::scope("/auth")
                    .service(auth::register)
                    .service(auth::login),
            ),
    );
}

7. Middlewares

7.1 Middleware de autenticación JWT

// src/utils/auth.rs
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use futures::future::{ready, Ready};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use std::env;

pub struct AuthenticatedUser {
    pub user_id: String,
}

impl FromRequest for AuthenticatedUser {
    type Error = Error;
    type Future = Ready<Result<Self, Self::Error>>;
    
    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
        let auth_header = req.headers().get("Authorization");
        
        if auth_header.is_none() {
            return ready(Err(actix_web::error::ErrorUnauthorized("Autorización requerida")));
        }
        
        let token = auth_header.unwrap().to_str().unwrap().replace("Bearer ", "");
        let secret = env::var("JWT_SECRET").expect("JWT_SECRET must be set");
        
        match decode::<Claims>(
            &token,
            &DecodingKey::from_secret(secret.as_ref()),
            &Validation::new(Algorithm::HS256),
        ) {
            Ok(decoded) => {
                ready(Ok(AuthenticatedUser {
                    user_id: decoded.claims.sub,
                }))
            }
            Err(_) => ready(Err(actix_web::error::ErrorUnauthorized("Token inválido"))),
        }
    }
}

7.2 Uso en handlers protegidos

// src/handlers/auth.rs
use crate::utils::auth::AuthenticatedUser;

#[get("/me")]
pub async fn get_current_user(
    auth_user: AuthenticatedUser,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ServiceError> {
    let user_id = mongodb::bson::oid::ObjectId::parse_str(&auth_user.user_id)
        .map_err(|_| ServiceError::BadRequest("ID de usuario inválido".into()))?;
    
    let user = state
        .db
        .collection::<User>("users")
        .find_one(doc! { "_id": user_id }, None)
        .await
        .map_err(|_| ServiceError::InternalServerError)?
        .ok_or(ServiceError::NotFound)?;
    
    Ok(HttpResponse::Ok().json(UserResponse::from(user)))
}

8. Documentación con Swagger

8.1 Anotar endpoints para Swagger

// src/handlers/auth.rs
use utoipa::ToSchema;

#[utoipa::path(
    post,
    path = "/api/v1/auth/register",
    request_body = RegisterUser,
    responses(
        (status = 201, description = "Usuario registrado exitosamente"),
        (status = 400, description = "Datos de usuario inválidos"),
        (status = 409, description = "Usuario o email ya existe"),
        (status = 500, description = "Error interno del servidor"),
    )
)]
#[post("/register")]
pub async fn register(
    data: web::Json<RegisterUser>,
    state: web::Data<AppState>,
) -> Result<HttpResponse, ServiceError> {
    // ... implementación
}

8.2 Configurar documentación OpenAPI

// src/main.rs
#[derive(OpenApi)]
#[openapi(
    paths(
        handlers::auth::register,
        handlers::auth::login,
        handlers::auth::get_current_user,
    ),
    components(
        schemas(
            models::user::RegisterUser,
            models::user::LoginUser,
            models::user::UserResponse,
            utils::auth::TokenResponse,
            errors::ServiceError,
        )
    ),
    tags(
        (name = "auth", description = "Autenticación de usuarios"),
    ),
    modifiers(&SecurityAddon)
)]
struct ApiDoc;

struct SecurityAddon;

impl utoipa::Modify for SecurityAddon {
    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
        if let Some(components) = openapi.components.as_mut() {
            components.add_security_scheme(
                "bearerAuth",
                utoipa::openapi::security::SecurityScheme::Http(
                    utoipa::openapi::security::Http::new("bearer")
                        .scheme("bearer")
                        .bearer_format("JWT"),
                ),
            )
        }
    }
}

9. Despliegue en Linux

9.1 Compilar para producción

# Compilación optimizada
cargo build --release

# El binario estará en:
./target/release/rust-mongodb-api

9.2 Configurar systemd

# /etc/systemd/system/rust-api.service
[Unit]
Description=Rust MongoDB API
After=network.target

[Service]
User=rustapi
Group=rustapi
WorkingDirectory=/opt/rust-api
ExecStart=/opt/rust-api/rust-mongodb-api
Restart=always
EnvironmentFile=/opt/rust-api/.env

[Install]
WantedBy=multi-user.target

9.3 Configurar Nginx como proxy inverso

# /etc/nginx/sites-available/rust-api
server {
    listen 80;
    server_name api.tudominio.com;
    
    location / {
        proxy_pass http://localhost:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    location /swagger/ {
        proxy_pass http://localhost:8080/swagger/;
    }
}

10. Conclusión

Hemos construido una API REST completa en Rust con:

Mejoras posibles: