Express.js
Request y response como objetos HTTP
Modelo mental de request y response como objetos HTTP vivos, mutables y ligados al ciclo de una petición.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Express.js
Modelo mental de request y response como objetos HTTP vivos, mutables y ligados al ciclo de una petición.
requestresponseExpress recibe de Node.js un IncomingMessage y un ServerResponse, los amplía y los entrega a cada middleware y handler. No son DTOs puros: son objetos vivos, basados en streams y compartidos por todas las capas de la misma request.
El modelo mental es:
bytes del cliente
↓
request de Node.js
↓ Express añade helpers
request de Express
↓ pipeline
response de Express
↓
bytes hacia el clienteSin una abstracción común, cada parte del servidor tendría que interpretar método, URL, headers, cuerpo y estado de la respuesta manualmente. Express centraliza esa información y ofrece helpers como request.params, request.query, response.status() y response.json().
El riesgo es tratar estos objetos como si fueran datos confiables o estructuras inmutables. Todo lo que proviene del cliente sigue siendo no confiable, y cualquier middleware puede modificar contexto visible para capas posteriores.
method: método HTTP.originalUrl, baseUrl, path: distintas vistas de la URL según el router.headers: metadata enviada por cliente o proxies.socket: conexión subyacente.aborted y close.params: variables capturadas por la ruta.query: resultado del query parser configurado.body: resultado de un body parser, si alguno coincidió.cookies: solo si se instaló middleware para interpretarlas.ip, protocol, hostname: valores que pueden depender de trust proxy.La aplicación puede añadir identidad, tenant, input validado o request ID. Para evitar contratos invisibles, conviene centralizar este contexto en response.locals o en una propiedad tipada claramente.
type RequestContext = {
requestId: string;
actor?: { id: string; roles: string[] };
tenantId?: string;
};Debes considerar no confiables:
request.paramsrequest.queryrequest.bodyrequest.headersTypeScript no cambia esta realidad:
const body = request.body as CreateOrderInput;Este cast solo silencia al compilador. No verifica que el cliente haya enviado un objeto ni que items sea un array válido.
La response se compone de:
status code
+ headers
+ body opcionalExpress proporciona helpers, pero la semántica sigue siendo HTTP:
response
.status(201)
.location(`/orders/${order.id}`)
.json({ data: order });json() serializa el valor, configura el media type y finaliza la respuesta. send(), end(), redirect(), sendFile() y un stream también pueden iniciar o cerrar la respuesta.
Todas las capas observan los mismos objetos. Un middleware puede hacer:
response.locals.requestId = crypto.randomUUID();
response.set('X-Request-Id', response.locals.requestId);Esto es útil para contexto transversal, pero introducir demasiadas mutaciones implícitas hace difícil saber qué propiedades existen y quién las produjo.
Una request debe tener una única salida terminal:
if (!order) {
response.status(404).json({ code: 'ORDER_NOT_FOUND' });
return;
}
response.status(200).json({ data: order });El return no es necesario para cerrar la red; es necesario para detener el flujo de JavaScript y evitar otra escritura.
response.headersSent indica que los headers ya fueron entregados al sistema. Después de eso:
if (response.headersSent) {
next(error);
return;
}El cliente puede desconectarse antes de recibir la respuesta. Esto no cancela automáticamente:
Puedes crear una señal de cancelación:
function createRequestSignal(request: Request): AbortSignal {
const controller = new AbortController();
request.on('aborted', () => controller.abort());
request.on('close', () => controller.abort());
return controller.signal;
}La señal solo sirve si la dependencia la acepta. Cancelar también puede ser más costoso que terminar una operación pequeña, por lo que debe aplicarse con criterio.
router.get('/:orderId', async (request, response) => {
const orderId = orderIdSchema.parse(request.params.orderId);
const signal = createRequestSignal(request);
const order = await findOrder({
orderId,
actor: response.locals.actor,
signal,
});
if (!order) {
response.status(404).json({ code: 'ORDER_NOT_FOUND' });
return;
}
response.status(200).json({ data: order });
});Flujo:
No pases request completo al caso de uso:
await createOrder.execute(request); // acoplamiento HTTPExtrae un input explícito:
await createOrder.execute({
actor: response.locals.actor,
input: response.locals.validatedBody,
});Esto permite ejecutar el caso de uso desde tests, jobs o CLI sin fabricar objetos Express.
Puede no haberse registrado un parser, el Content-Type no coincidió o el body estaba ausente. El handler debe depender de una etapa de validación, no asumir existencia.
El parser puede devolver string, array u otra estructura según configuración. Normaliza mediante un schema.
Host, Origin, X-Forwarded-For y User-Agent pueden falsificarse. Solo confía cuando existe una política y una infraestructura conocidas.
Un error posterior debe registrarse y cerrar o delegar; no intentes enviar otro contrato completo.
Añadir decenas de propiedades sin contrato crea dependencias ocultas.
Request<Params, ResBody, ReqBody, Query> mejora autocompletado, pero no valida runtime.
Provoca doble response o side effects posteriores al resultado HTTP.
Mezcla usuarios bajo concurrencia. Usa contexto por request.
Puede producir open redirects o host header injection.
Una prueba de integración debe cubrir:
headersSent limita lo que puede corregirse después.request.body as Input no es validación?request.path, baseUrl y originalUrl?headersSent sea true?res.locals?originalUrl conserva la URL original.Ciclo de vida de una petición explica cómo estos objetos recorren el stack y por qué el orden cambia el comportamiento.