study.plynte.com
@franguh
Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)

06 · Actix: límites entre HTTP y dominio#

Objetivo#

Al terminar esta lección podrás diseñar e implementar un endpoint web robusto con Actix Web manteniendo una separación absoluta entre la frontera de transporte HTTP y el núcleo de dominio en Rust puro. Comprenderás la mecánica interna del trait FromRequest y de los extractores, sabrás en qué momento exacto del ciclo de vida se rechaza una solicitud sintácticamente rota frente a una semánticamente inválida, y podrás justificar con rigor técnico qué garantías ofrece una prueba en memoria con test::init_service y qué propiedades de red quedan fuera de su alcance.


Antes de empezar#

  1. Completa 05 · Contratos HTTP y ciclo de una solicitud y 03 · Modelado de errores y pruebas.
  2. Esta lección divide sus ejemplos de código en dos categorías explícitas: - Módulo de dominio puro (Standard Library): Código Rust estándar autónomo que puedes compilar y verificar directamente con rustc --test. - Frontera de transporte con Actix Web 4: Fragmentos respaldados por dependencias externas (actix-web = "4", serde = { version = "1.0", features = ["derive"] }) diseñados para proyectos gestionados con cargo.
  3. Si encuentras algún concepto técnico desconocido, consulta el glosario técnico.

Diccionario de conceptos de la lección (Glosario desde cero)#

1. Frontera de transporte (HTTP Boundary) vs Núcleo de dominio (Domain Core)#

  • Intuición cotidiana: En una embajada, la frontera de transporte es la ventanilla de recepción en la aduana: revisa que el pasaporte no sea una fotocopia borrosa y que los sellos sean legibles. El núcleo de dominio es el juez o cónsul en su oficina privada: no habla con desconocidos en la acera, no le importa de qué color es la carpeta física y solo decide si el solicitante cumple las leyes de asilo o ciudadanía.
  • Definición técnica precisa:
  • *Frontera de transporte (HTTP Boundary):* Capa periférica del software encargada de interactuar con el protocolo de red. Sus responsabilidades son: mapear URLs a funciones, deserializar bytes a DTOs, extraer cabeceras/parámetros, capturar errores de dominio y serializar resultados a respuestas HTTP con códigos de estado (200, 400, etc.). Depende directamente del framework web (actix_web).
  • *Núcleo de dominio (Domain Core): Capa central del sistema donde residen las entidades, tipos refinados e invariantes del negocio. Es código Rust puro (std), determinista y libre de efectos secundarios de red. Nunca debe importar tipos como HttpRequest, HttpResponse o macros de Actix Web*.
  • Ejemplo conceptual en Rust:
RUST
  // Dominio: Rust puro, sin dependencias web
  pub fn validate_title(raw: &str) -> Result<String, TitleError> { /* ... */ }

  // Frontera: Actix Web, coordina transporte y mapea errores a HTTP
  async fn handle_post(body: web::Json<TitleDto>) -> HttpResponse { /* ... */ }
  • Concepción errónea: Pensar que "separar en capas" requiere obligatoriamente microservicios o diez carpetas con interfaces abstractas dinámicas (Box<dyn Trait>). La frontera es una disciplina de dependencias: el transporte depende del dominio; el dominio no sabe que HTTP existe.

2. Handler en Actix Web#

  • Intuición cotidiana: El recepcionista asignado a una ventanilla específica (ej. "Ventanilla 3: Renovación de licencias"). Cuando llega un ciudadano con el formulario correspondiente, lo atiende, llama al funcionario de archivo y le entrega al ciudadano su comprobante sellado.
  • Definición técnica precisa: Función asíncrona en Rust (async fn) que satisface el trait Handler de Actix Web. Recibe de cero a múltiples argumentos que implementan el trait FromRequest (los extractores), ejecuta la orquestación del caso de uso y retorna un tipo que implementa el trait Responder (típicamente HttpResponse).
  • Ejemplo en código:
RUST
  async fn health_check() -> impl actix_web::Responder {
      actix_web::HttpResponse::Ok().body("healthy")
  }
  • Concepción errónea: Poner la lógica de negocio, cálculos matemáticos y consultas complejas de base de datos directamente dentro del cuerpo del handler. El handler debe ser únicamente un orquestador o pegamento (glue code).

3. Extractor (web::Json<T>, web::Path<T>, web::Query<T> y el trait FromRequest)#

  • Intuición cotidiana: Un filtro de aduana especializado: un extractor de maletas pesadas (web::Json), un extractor del número de asiento del boleto (web::Path), o un extractor de cupones de descuento adjuntos (web::Query). Si tu maleta no cabe en la máquina o viene rota, el filtro te devuelve en la puerta antes de que puedas pisar la sala de espera.
  • Definición técnica precisa: Tipo utilitario proporcionado por Actix Web que implementa el trait FromRequest. Antes de que el handler comience a ejecutarse, Actix invoca FromRequest::from_request(&req, &mut payload). El extractor inspecciona la solicitud HTTP, extrae los fragmentos correspondientes (cuerpo, segmento de ruta, parámetros de consulta) y los convierte en tipos tipados de Rust. Si la extracción falla, la canalización (pipeline) se interrumpe y Actix devuelve inmediatamente una respuesta de error al cliente sin llegar a ejecutar el cuerpo del handler.
  • Ejemplo en código:
RUST
  async fn update_user(
      path: web::Path<u32>,        // Extrae /users/{id}
      payload: web::Json<UserDto>, // Extrae y deserializa el cuerpo JSON
  ) -> HttpResponse { /* ... */ }
  • Concepción errónea: Creer que si web::Json<T> tiene éxito, los datos contenidos en T ya son válidos para el negocio. Solo garantiza que la sintaxis JSON es correcta y coincide con los tipos del struct.

4. DTO (Data Transfer Object) vs Entidad de dominio#

  • Intuición cotidiana:
  • DTO: El sobre acolchado con el que viaja un reloj de mano por correo postal (con etiquetas de remitente, código de barras y cinta adhesiva).
  • Entidad de dominio: El reloj de mano en sí mismo (con sus engranajes, manecillas y mecanismo de precisión funcionando).
  • Definición técnica precisa:
  • *DTO (Data Transfer Object):* Estructura de datos pública y anémica diseñada exclusivamente para transportar datos a través de la red o entre procesos. Suele implementar Deserialize y Serialize, permite campos opcionales o tipos primitivos abiertos (String, i32) y no contiene lógica de negocio ni invariantes.
  • *Entidad de dominio (Domain Entity / Value Object):* Tipo en Rust que encapsula invariantes estrictas del negocio mediante visibilidad restringida (campos privados) y constructores que retornan Result. Garantiza que es físicamente imposible instanciar un estado inválido en la memoria de la aplicación.
  • Ejemplo en código:
RUST
  // DTO (Frontera HTTP)
  #[derive(serde::Deserialize)]
  pub struct CreateTaskRequest {
      pub title: String,
  }

  // Value Object (Dominio)
  #[derive(Debug, PartialEq, Eq)]
  pub struct TaskTitle(String);

  impl TaskTitle {
      pub fn parse(raw: &str) -> Result<Self, &'static str> {
          let trimmed = raw.trim();
          if trimmed.is_empty() {
              return Err("title cannot be empty");
          }
          Ok(Self(trimmed.to_string()))
      }
  }
  • Concepción errónea: Intentar utilizar la misma estructura con #[derive(Deserialize)] directamente como entidad de base de datos o entidad de dominio. Esto acopla la representación pública de la API con los invariantes internos del sistema.

5. Serde (#[derive(Deserialize, Serialize)])#

  • Intuición cotidiana: Un traductor universal instantáneo: toma un texto escrito en jeroglíficos JSON y lo traduce a una ficha de cartón en tu idioma; o toma tu ficha y redacta el texto JSON para enviarlo por telégrafo.
  • Definición técnica precisa: Framework canónico de serialización y deserialización de datos para Rust (SERialization / DEserialization). Mediante macros de derivación procedural (derive), genera código de alto rendimiento en tiempo de compilación para convertir entre representaciones en memoria de tipos Rust y formatos de datos estructurados (JSON, YAML, Postcard, BSON) sin costo de introspección dinámica (zero-cost abstractions).
  • Ejemplo en código:
RUST
  use serde::{Deserialize, Serialize};

  #[derive(Deserialize, Serialize)]
  pub struct NoteResponse {
      pub id: u64,
      pub title: String,
  }
  • Concepción errónea: Pensar que Serde realiza validaciones complejas de negocio. Si declaras pub age: u8, Serde aceptará el valor 255 porque sintácticamente cabe en un byte sin signo, aunque en tu dominio una edad humana de 255 años sea inválida.

6. Trait Responder y HttpResponse#

  • Intuición cotidiana: La orden formal que firma un funcionario para autorizar la salida de un paquete del edificio con el sello oficial del ministerio.
  • Definición técnica precisa:
  • HttpResponse: Estructura concreta en Actix Web que representa una respuesta HTTP completa construida en el servidor: línea de estado, cabeceras y cuerpo de bytes. Ofrece un patrón constructor fluido (builder pattern), por ejemplo HttpResponse::Ok().json(payload).
  • Responder: Trait que define cómo un tipo de Rust puede convertirse en un HttpResponse. Si una función devuelve un tipo que implementa Responder (como &'static str, String, o web::Json<T>), Actix invoca automáticamente respond_to(&self, &HttpRequest) para armar el paquete de red final.
  • Ejemplo en código:
RUST
  HttpResponse::Created()
      .insert_header(("Location", "/notes/42"))
      .json(NoteDto { id: 42, title: "Rust".into() })
  • Concepción errónea: Creer que retornar HttpResponse::BadRequest() lanza un error o interrumpe la ejecución del hilo. HttpResponse es simplemente un valor normal de retorno; el control de flujo debe gestionarse con la palabra clave return o expresiones de bifurcación condicional.

7. Runtime asíncrono (Tokio / Actix)#

  • Intuición cotidiana: Un director de orquesta que supervisa a 100 músicos: si el pianista está esperando que pasen la página de su partitura, el director no detiene el concierto, sino que le da la señal al violinista para que toque su solo mientras tanto.
  • Definición técnica precisa: Entorno de ejecución en espacio de usuario que gestiona hilos de trabajo del sistema operativo (OS worker threads), un bucle de eventos (event loop) y un reactor de E/S (I/O reactor, como epoll en Linux o IOCP en Windows). Actix Web se ejecuta sobre el runtime tokio. Permite que miles de tareas asíncronas cooperativas se multiplexen eficientemente sobre un número pequeño de hilos nativos del sistema operativo (habitualmente un hilo por cada núcleo físico de la CPU).
  • Ejemplo conceptual:

#[actix_web::main] inicializa el pool de hilos de Tokio y lanza el supervisor de red de Actix.

  • Concepción errónea: Suponer que Tokio convierte automáticamente cualquier función lenta en asíncrona. Si ejecutas una operación intensiva de CPU o una llamada síncrona bloqueante (std::thread::sleep), congelarás el hilo del worker y detendrás a todas las demás tareas que compartían ese mismo hilo.

8. Prueba de servicio en memoria (test::init_service) vs Servidor de red real#

  • Intuición cotidiana:
  • Prueba en memoria: Probar el motor de un automóvil en un banco de pruebas de taller conectándolo directamente a mangueras de combustible y sensores eléctricos simulados.
  • Prueba de red real: Poner el automóvil en la autopista bajo lluvia real, con baches, tráfico y viento.
  • Definición técnica precisa:
  • test::init_service: Función del módulo de pruebas de Actix Web que inicializa la estructura App con sus rutas, middlewares y handlers en la memoria del propio proceso. Permite enviar instancias de TestRequest llamando directamente al trait Service::call de la aplicación sin abrir sockets TCP ni interactuar con la pila de red del sistema operativo.
  • Servidor de red real: Proceso que enlaza un puerto del kernel (bind("0.0.0.0:8080")), escucha conexiones entrantes (listen) y realiza transferencias de paquetes TCP/TLS reales mediante un cliente HTTP externo (reqwest o curl).
  • Tabla comparativa de cobertura:
Dimensión de pruebatest::init_service (En memoria)Servidor real en puerto de red
Enrutamiento y coincidencia de patrones URISíSí
Deserialización y extractores (FromRequest)SíSí
Middlewares y mapeo de HttpResponseSíSí
Velocidad de ejecución en suite de pruebasUltrarrápida (~milisegundos)Lenta (~segundos)
Apertura y saturación de sockets de red del SONoSí
Negociación TLS / Certificados HTTPSNoSí
Configuración de proxies inversos / Nginx / CORS realNoSí
Comportamiento ante fragmentación de paquetes TCPNoSí
  • Concepción errónea: Afirmar que "mi aplicación soporta tráfico seguro HTTPS y está lista para producción" basándose exclusivamente en pruebas exitosas con test::init_service.

Modelo mental y arquitectura profunda#

Qué ocurre dentro de Actix Web antes de que el handler se ejecute#

Es fundamental comprender la secuencia interna de evaluación que Actix Web ejecuta sobre cada solicitud entrante:

DIAGRAMA MERMAID
flowchart TD
    Req[Solicitud HTTP recibida] --> MatchRoute{¿Coincide la ruta y el método?}
    MatchRoute -- No --> Err404[Retorna 404 Not Found / 405 Method Not Allowed]
    MatchRoute -- Sí --> ExtractorCall[Invoca FromRequest::from_request]
    
    subgraph PipelineExtractor["Pipeline del Extractor (web::Json)"]
        ExtractorCall --> ReadBytes[Drena buffer de bytes desde el socket]
        ReadBytes -- Falla I/O o Timeout --> ErrStream[Retorna 400 / 408]
        ReadBytes -- Exitoso --> CheckHeader{¿Content-Type es application/json?}
        CheckHeader -- No --> Err415[Retorna 415 Unsupported Media Type]
        CheckHeader -- Sí --> SerdeParse[Invoca serde_json::from_slice]
        SerdeParse -- Sintaxis corrupta o tipo erróneo --> Err400[Retorna 400 Bad Request]
    end
    
    SerdeParse -- Deserialización OK --> InvokeHandler[Llama a la función handler async con web::Json DTO]
    
    subgraph ExecutionHandler["Cuerpo del Handler (Orquestación)"]
        InvokeHandler --> DomainValidation[Invoca validate_title en el Núcleo de Dominio]
        DomainValidation -- Err: Violación de regla de negocio --> MapErr[Handler mapea a HttpResponse::BadRequest / 422]
        DomainValidation -- Ok: Invariante satisfecha --> SuccessLogic[Lógica de caso de uso]
        SuccessLogic --> MapOk[Handler mapea a HttpResponse::Ok]
    end
    
    Err404 --> ClientResponse[Respuesta enviada al cliente]
    ErrStream --> ClientResponse
    Err415 --> ClientResponse
    Err400 --> ClientResponse
    MapErr --> ClientResponse
    MapOk --> ClientResponse

El contraste entre dos fallos comunes:#

  1. Fallo de deserialización sintáctica: Si el cliente envía el cuerpo: ``json {"title": 12345} ` mientras que el DTO espera pub title: String, el parser de Serde falla durante el paso FromRequest::from_request. Actix Web intercepta este error, genera automáticamente un HttpResponse::BadRequest()` y aborta la petición. Tu handler jamás es llamado; ninguna de tus líneas de código se ejecuta.
  2. Fallo de validación semántica de dominio: Si el cliente envía: ``json {"title": " "} ` el extractor tiene un éxito rotundo: " " es un string JSON sintácticamente perfecto. Serde construye la instancia de ValidateNoteTitleRequest y Actix invoca tu handler. Es ahora tu responsabilidad explícita invocar la función de dominio pura validate_title(&payload.title), identificar que la cadena contiene únicamente espacios y emitir el código HTTP apropiado (400 Bad Request o 422 Unprocessable Content`).

Diagrama#

DIAGRAMA MERMAID
flowchart LR
    A[Bytes JSON] --> B[Extractor Actix]
    B -->|parse error| E[Error HTTP]
    B -->|datos tipados| C[Validación dominio]
    C -->|regla rechazada| F[Error dominio]
    C -->|comando válido| D[Resultado dominio]
    D --> G[Mapeo a respuesta HTTP]

Diagrama estático del tema
Diagrama estático del tema
Fuente editable: Mermaid.

La frontera actúa como un aislante eléctrico: absorbe la complejidad y volatilidad de la red, garantizando que el núcleo de dominio solo reciba y devuelva estructuras fuertemente tipadas y comprensibles.


Ejemplo razonado#

A continuación analizamos los dos componentes de la solución: primero, el núcleo de dominio en Rust estándar autónomo; segundo, la frontera de transporte con Actix Web.

Componente 1: Núcleo de dominio puro (Rust Standard Library autónomo)#

Este bloque no contiene ninguna dependencia de Actix ni de Serde. Puedes guardarlo como domain_title.rs y verificar su compilación y ejecución de pruebas con rustc --test domain_title.rs:

RUST
// Contract: Domain entity representing a validated, non-empty note title.
// Invariant: Title must not be empty after trimming whitespace, and must not exceed 100 characters.

#[derive(Debug, PartialEq, Eq)]
pub enum TitleError {
    Empty,
    TooLong { max: usize, actual: usize },
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ValidatedTitle(String);

impl ValidatedTitle {
    pub fn parse(raw: &str) -> Result<Self, TitleError> {
        let trimmed = raw.trim();
        if trimmed.is_empty() {
            return Err(TitleError::Empty);
        }
        if trimmed.len() > 100 {
            return Err(TitleError::TooLong {
                max: 100,
                actual: trimmed.len(),
            });
        }
        Ok(Self(trimmed.to_string()))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn rejects_empty_or_whitespace_strings() {
        assert_eq!(ValidatedTitle::parse(""), Err(TitleError::Empty));
        assert_eq!(ValidatedTitle::parse("   "), Err(TitleError::Empty));
        assert_eq!(ValidatedTitle::parse("\t\n"), Err(TitleError::Empty));
    }

    #[test]
    fn accepts_valid_trimmed_titles() {
        let result = ValidatedTitle::parse("  Valid Note Title  ");
        assert_eq!(result, Ok(ValidatedTitle("Valid Note Title".to_string())));
        assert_eq!(result.unwrap().as_str(), "Valid Note Title");
    }

    #[test]
    fn rejects_strings_exceeding_maximum_length() {
        let exact_100 = "a".repeat(100);
        assert!(ValidatedTitle::parse(&exact_100).is_ok());

        let oversized_101 = "a".repeat(101);
        assert_eq!(
            ValidatedTitle::parse(&oversized_101),
            Err(TitleError::TooLong { max: 100, actual: 101 })
        );
    }
}

Por qué este diseño es robusto:

  • Las pruebas de dominio se ejecutan en 0 milisegundos sin levantar un runtime asíncrono ni simular solicitudes HTTP.
  • Si mañana decides reemplazar Actix Web por Axum o construir una herramienta de línea de comandos (CLI), este código se reutiliza exactamente igual sin cambiar una sola coma.

Componente 2: Frontera de transporte con Actix Web 4 (Crate-backed)#

El siguiente fragmento ilustra cómo la capa HTTP orquesta el extractor, delega en el dominio y traduce los errores a respuestas HTTP estructuradas en un proyecto Cargo con dependencias actix-web = "4" y serde = { version = "1.0", features = ["derive"] }:

RUST
// Framework boundary: Actix Web 4 + Serde
// Requires: actix-web 4.x and serde in Cargo.toml
use actix_web::{web, App, HttpResponse, Responder};
use serde::{Deserialize, Serialize};

// Import domain core from the pure module
// use crate::domain_title::{ValidatedTitle, TitleError};

#[derive(Deserialize)]
pub struct ValidateNoteTitleRequest {
    pub title: String,
}

#[derive(Serialize)]
pub struct ApiErrorResponse {
    pub error: String,
    pub detail: String,
}

#[derive(Serialize)]
pub struct ApiSuccessResponse {
    pub message: String,
    pub sanitized_title: String,
}

// HTTP Handler: Translates transport into domain, and domain into transport
pub async fn validate_note_title_handler(
    payload: web::Json<ValidateNoteTitleRequest>,
) -> HttpResponse {
    match ValidatedTitle::parse(&payload.title) {
        Ok(valid_title) => HttpResponse::Ok().json(ApiSuccessResponse {
            message: "title valid".to_string(),
            sanitized_title: valid_title.as_str().to_string(),
        }),
        Err(TitleError::Empty) => HttpResponse::BadRequest().json(ApiErrorResponse {
            error: "validation_error".to_string(),
            detail: "title must not be empty or contain only whitespace".to_string(),
        }),
        Err(TitleError::TooLong { max, actual }) => {
            HttpResponse::BadRequest().json(ApiErrorResponse {
                error: "validation_error".to_string(),
                detail: format!("title length ({actual}) exceeds maximum allowed ({max})"),
            })
        }
    }
}

Práctica independiente#

Construye un laboratorio práctico en tu entorno de trabajo siguiendo la disciplina guiada por pruebas:

Fase 1: Núcleo de dominio puro (RED -> GREEN -> REFACTOR)#

  1. Crea un archivo domain.rs.
  2. Escribe una función de validación de título validate_title(raw: &str) -> Result<String, TitleError>.
  3. Escribe las tres aserciones de prueba en un módulo mod tests: - Cadena "Rust" debe retornar Ok("Rust".to_string()). - Cadena "" debe retornar Err(TitleError::Empty). - Cadena " " debe retornar Err(TitleError::Empty).
  4. Compila y ejecuta con rustc --test domain.rs hasta obtener test result: ok. 3 passed.

Fase 2: Configuración del proyecto Cargo y frontera Actix Web#

  1. Inicializa un proyecto con cargo new actix_boundary_lab --bin.
  2. En Cargo.toml, agrega las dependencias: ``toml [dependencies] actix-web = "4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" ``
  3. Registra la ruta en tu aplicación: ``rust App::new().service( web::resource("/notes/validate").route(web::post().to(validate_note_title_handler)) ) ``

Fase 3: Pruebas de servicio en memoria con actix_web::test#

Escribe una batería de pruebas de integración con test::init_service que cubra rigurosamente los tres límites de transporte:

  1. Prueba de éxito: Enviar POST /notes/validate con cabecera Content-Type: application/json y cuerpo {"title": "Valid Note"}. - Aserción: El código HTTP devuelto debe ser exactamente 200 OK.
  2. Prueba de fallo de dominio: Enviar {"title": " "}. - Aserción: El código devuelto debe ser 400 Bad Request y el cuerpo JSON debe contener el detalle de error.
  3. Prueba de fallo de extractor: Enviar JSON sintácticamente roto {"title": 123} o {"bad_json":. - Aserción: El código devuelto debe ser 400 Bad Request generado directamente por el extractor de Actix Web.

Ejecuta cargo test y confirma que todas las pruebas pasen satisfactoriamente. Contrasta tu diseño contra la guía de soluciones de backend.


Preguntas de comprensión profunda y diagnóstico socrático#

Reflexiona sobre estas cinco preguntas fundamentales antes de continuar:

Pregunta 1: Mecánica de interrupción en el trait FromRequest#

Examina la firma conceptual del extractor de Actix:

RUST
impl<T: DeserializeOwned> FromRequest for web::Json<T> {
    type Error = actix_web::Error;
    // ...
}
  • ¿Qué sucede a nivel de memoria y asignación de buffers si el cliente envía un flujo de bytes que finaliza abruptamente a mitad de un objeto JSON?
  • Si un atacante envía un cuerpo JSON válido de 500 megabytes a un endpoint que utiliza web::Json<T>, ¿en qué momento detiene Actix la solicitud y qué extractor o configuración de límite (web::JsonConfig::limit) previene el agotamiento de memoria del servidor?

Pregunta 2: Aislamiento arquitectónico y tipos de framework#

Imagina que un programador decide ahorrar líneas de código declarando su función de dominio de la siguiente manera:

RUST
// Código problemático
pub fn validate_and_respond(title: &str) -> actix_web::HttpResponse {
    if title.trim().is_empty() {
        actix_web::HttpResponse::BadRequest().body("Empty title")
    } else {
        actix_web::HttpResponse::Ok().body("Valid title")
    }
}
  • ¿Por qué esta función es imposible de reutilizar si la empresa decide crear una herramienta CLI o migrar a una arquitectura basada en gRPC o AWS Lambda?
  • ¿Cómo afecta esta firma a la suite de pruebas unitarias en términos de dependencias y tiempos de compilación?

Pregunta 3: Alcance y límites de test::init_service#

Un ingeniero afirma: "Nuestra suite de integración tiene cobertura del 100% usando test::init_service; por ende, hemos demostrado que la aplicación soporta balanceo de carga con Nginx y no tiene problemas de certificados SSL ni de CORS".

  • Explica qué pasos de la pila de red del sistema operativo se omiten completamente al llamar a test::init_service.
  • Si el servidor tiene configurado un puerto privilegiado (ej. 80) pero el usuario del sistema operativo que ejecuta el binario carece de permisos de administrador (root), ¿detectará este problema la prueba en memoria? ¿Por qué?

Pregunta 4: Deserialización vs Invariantes de Tipo Nuevo (Newtype Pattern)#

Compara estas dos declaraciones para el parámetro de un handler:

  • Opción A: async fn create_user(body: web::Json<RawUserDto>) donde RawUserDto tiene campos públicos pub email: String.
  • Opción B: async fn create_user(body: web::Json<RawUserDto>) donde inmediatamente se invoca Email::parse(body.email).
  • ¿Qué peligro existe si una función auxiliar de la base de datos recibe un String genérico en lugar de un tipo formal Email?
  • ¿Cómo previene el compilador de Rust que un valor no validado llegue a la capa de persistencia cuando se aplican constructores con visibilidad privada?

Pregunta 5: La semántica de web::Data<T> y el estado compartido#

Si un handler necesita acceder a una base de datos o a un contador en memoria a través de web::Data<AppState>:

  • ¿Por qué Actix Web exige que los datos dentro de web::Data implementen Clone o se almacenen dentro de un Arc?
  • ¿Cuántos hilos de trabajo (worker threads) crea Actix Web por defecto en una máquina con 8 núcleos de procesador y cómo interactúan esos hilos con la instancia de App creada en el closure de fábrica?

Code smell#

Síntoma: Fuga de tipos de transporte HTTP dentro de la capa de dominio#

En un proyecto, encuentras la siguiente función dentro de un archivo services/billing.rs:

RUST
// Code smell crítico: tipos de Actix en el dominio
use actix_web::{HttpResponse, ResponseError};

pub fn process_payment(amount: u32) -> Result<Invoice, HttpResponse> {
    if amount == 0 {
        return Err(HttpResponse::UnprocessableEntity().body("Amount must be positive"));
    }
    // ...
    Ok(invoice)
}
  • Evidencia observable: La lógica de facturación importa HttpResponse y genera respuestas HTTP directamente en el núcleo de la aplicación.
  • Hipótesis del problema: El desarrollador inicial quiso "simplificar" el código del handler devolviendo directamente el error HTTP listo para enviar al cliente.
  • Consecuencia concreta:
  1. No es posible invocar process_payment desde un worker asíncrono de cola (como Celery, SQS o RabbitMQ), ni desde una prueba unitaria ligera de std.
  2. Si el formato de error de la API cambia (por ejemplo, para adoptar RFC 9457 application/problem+json), hay que modificar todos los archivos del negocio en lugar de un único mapeador en la frontera HTTP.
  • Tratamiento mínimo justificado:
  1. Definir un enum de error de dominio puro: pub enum PaymentError { ZeroAmount, InsufficientFunds }.
  2. Hacer que process_payment retorne Result<Invoice, PaymentError>.
  3. En el handler de Actix Web, realizar el match y traducir cada variante de PaymentError al código y formato HTTP correspondiente (422, 402, etc.).

Criterio de salida#

Puedes considerar superada esta lección con maestría cuando:

  • Tu código de dominio compila y supera todas sus pruebas unitarias de frontera usando exclusivamente herramientas estándar (rustc --test).
  • Eres capaz de explicar con diagramas de flujo la diferencia entre un fallo en el extractor de Actix (FromRequest) y un fallo en la validación semántica del handler.
  • Has configurado un servicio de prueba con test::init_service y validado exitosamente los tres escenarios de transporte (200 OK, 400 Bad Request por dominio, y rechazo por JSON mal formado).
  • Puedes enumerar con precisión al menos cuatro aspectos de red que una prueba en memoria jamás podrá verificar (enlace de socket, certificados TLS, CORS de navegador, latencia de paquete).
  • Tu handler no contiene reglas de negocio duplicadas y tu dominio no importa ningún tipo de actix_web.

Lecturas oficiales#


Siguiente paso#

Continúa con 07 · PostgreSQL: invariantes y transacciones, donde trasladarás la persistencia a una base de datos relacional y aprenderás a proteger invariantes de negocio frente a condiciones de carrera concurrentes utilizando transacciones ACID y restricciones declarativas.

Estado de Lectura Marca tu progreso local en tu navegador (iPad / Móvil / Desktop)