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#
- Completa 05 · Contratos HTTP y ciclo de una solicitud y 03 · Modelado de errores y pruebas.
- 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 concargo. - 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 comoHttpRequest,HttpResponseo macros de Actix Web*. - Ejemplo conceptual en 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 traitHandlerde Actix Web. Recibe de cero a múltiples argumentos que implementan el traitFromRequest(los extractores), ejecuta la orquestación del caso de uso y retorna un tipo que implementa el traitResponder(típicamenteHttpResponse). - Ejemplo en código:
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 invocaFromRequest::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:
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 enTya 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
DeserializeySerialize, 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:
// 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:
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 valor255porque 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 ejemploHttpResponse::Ok().json(payload).Responder: Trait que define cómo un tipo de Rust puede convertirse en unHttpResponse. Si una función devuelve un tipo que implementaResponder(como&'static str,String, oweb::Json<T>), Actix invoca automáticamenterespond_to(&self, &HttpRequest)para armar el paquete de red final.- Ejemplo en código:
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.HttpResponsees simplemente un valor normal de retorno; el control de flujo debe gestionarse con la palabra clavereturno 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
epollen Linux oIOCPen Windows). Actix Web se ejecuta sobre el runtimetokio. 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 estructuraAppcon sus rutas, middlewares y handlers en la memoria del propio proceso. Permite enviar instancias deTestRequestllamando directamente al traitService::callde 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 (reqwestocurl). - Tabla comparativa de cobertura:
| Dimensión de prueba | test::init_service (En memoria) | Servidor real en puerto de red |
|---|---|---|
| Enrutamiento y coincidencia de patrones URI | Sí | Sí |
Deserialización y extractores (FromRequest) | Sí | Sí |
Middlewares y mapeo de HttpResponse | Sí | Sí |
| Velocidad de ejecución en suite de pruebas | Ultrarrápida (~milisegundos) | Lenta (~segundos) |
| Apertura y saturación de sockets de red del SO | No | Sí |
| Negociación TLS / Certificados HTTPS | No | Sí |
| Configuración de proxies inversos / Nginx / CORS real | No | Sí |
| Comportamiento ante fragmentación de paquetes TCP | No | Sí |
- 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:
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:#
- Fallo de deserialización sintáctica:
Si el cliente envía el cuerpo:
``
json {"title": 12345}`mientras que el DTO esperapub title: String, el parser de Serde falla durante el pasoFromRequest::from_request. Actix Web intercepta este error, genera automáticamente unHttpResponse::BadRequest()` y aborta la petición. Tu handler jamás es llamado; ninguna de tus líneas de código se ejecuta. - 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 deValidateNoteTitleRequesty Actix invoca tu handler. Es ahora tu responsabilidad explícita invocar la función de dominio puravalidate_title(&payload.title), identificar que la cadena contiene únicamente espacios y emitir el código HTTP apropiado (400 Bad Requesto422 Unprocessable Content`).
Diagrama#
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]
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:
// 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"] }:
// 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)#
- Crea un archivo
domain.rs. - Escribe una función de validación de título
validate_title(raw: &str) -> Result<String, TitleError>. - Escribe las tres aserciones de prueba en un módulo
mod tests: - Cadena"Rust"debe retornarOk("Rust".to_string()). - Cadena""debe retornarErr(TitleError::Empty). - Cadena" "debe retornarErr(TitleError::Empty). - Compila y ejecuta con
rustc --test domain.rshasta obtenertest result: ok. 3 passed.
Fase 2: Configuración del proyecto Cargo y frontera Actix Web#
- Inicializa un proyecto con
cargo new actix_boundary_lab --bin. - En
Cargo.toml, agrega las dependencias: ``toml [dependencies] actix-web = "4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0"`` - 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:
- Prueba de éxito: Enviar
POST /notes/validatecon cabeceraContent-Type: application/jsony cuerpo{"title": "Valid Note"}. - Aserción: El código HTTP devuelto debe ser exactamente200 OK. - Prueba de fallo de dominio: Enviar
{"title": " "}. - Aserción: El código devuelto debe ser400 Bad Requesty el cuerpo JSON debe contener el detalle de error. - Prueba de fallo de extractor: Enviar JSON sintácticamente roto
{"title": 123}o{"bad_json":. - Aserción: El código devuelto debe ser400 Bad Requestgenerado 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:
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:
// 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>)dondeRawUserDtotiene campos públicospub email: String. - Opción B:
async fn create_user(body: web::Json<RawUserDto>)donde inmediatamente se invocaEmail::parse(body.email). - ¿Qué peligro existe si una función auxiliar de la base de datos recibe un
Stringgenérico en lugar de un tipo formalEmail? - ¿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::DataimplementenCloneo se almacenen dentro de unArc? - ¿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
Appcreada 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:
// 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
HttpResponsey 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:
- No es posible invocar
process_paymentdesde un worker asíncrono de cola (como Celery, SQS o RabbitMQ), ni desde una prueba unitaria ligera destd. - 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:
- Definir un enum de error de dominio puro:
pub enum PaymentError { ZeroAmount, InsufficientFunds }. - Hacer que
process_paymentretorneResult<Invoice, PaymentError>. - En el handler de Actix Web, realizar el
matchy traducir cada variante dePaymentErroral 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_servicey validado exitosamente los tres escenarios de transporte (200 OK,400 Bad Requestpor 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#
- Actix Web: Guía de inicio rápido (Getting Started): Aplicación, rutas, handlers y extractores.
- Actix Web: Testing: Comparativa detallada entre pruebas en memoria (
init_service) y pruebas de servidor real. - Referencia de Actix Web en docs.rs: Especificación técnica de los traits
FromRequest,HandleryResponder. - Glosario técnico del curso: Definiciones formales de arquitectura backend.
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.