LogoCésar Alberca

Opciones de Casos de Uso para Middlewares Avanzados

2026-02-04T15:00:00.000Z

En entregas anteriores de la Newsletter hablamos extensamente sobre los Casos de Uso y las tremendas capacidades que aportan a tu Arquitectura Frontend.

También hemos visto el método preferido para reconfigurar los casos de uso utilizando Middlewares, sin embargo, todavía falta algo.

Imaginemos que tenemos un caso de uso que registra información sensible en la consola, y queremos omitir su registro. Todavía queremos mantener el resto de los middlewares. ¿Cómo podríamos resolver este problema?

Primero podríamos pensar en añadir una sentencia if en el LoggerMiddleware:

export class LoggerMiddleware implements Middleware { async intercept(params: unknown, useCase: UseCase): Promise<unknown> { const useCaseName = useCase.constructor.name if (useCaseName === 'LoginCmd') { return useCase.handle(params) } console.log('Logging use case:', useCaseName) console.log('Logging params:', params) console.time(useCaseName) const result = await useCase.handle(params) console.timeEnd(useCaseName) console.log('Logging result:', result) return result } }

Esto no parece muy escalable, ¿verdad? Debe haber otra forma. Parece que deberíamos ser capaces de añadir contexto a la ejecución de los casos de uso. ¡Intentemos eso!

#Hacer que la ejecución de los Casos de Uso sea consciente del contexto

Introduzcamos el concepto de UseCaseOptions:

export type LogLevel = 'silent' | 'info' export interface UseCaseOptions { logLevel?: LogLevel }

UseCaseOptions representa un objeto que tiene opciones necesarias para los UseCases, el UseCaseService o los Middlewares.

A continuación, tenemos que averiguar dónde añadir esta información. Para hacerlo, me gusta pensar en el diseño de la API desde la perspectiva del cliente.

Necesitamos pasar de esto:

useCaseService.execute(loginCmd)

A esto:

useCaseService.execute(loginCmd, { logLevel: 'silent' })

Una vez que tenemos el qué, tenemos que dar con el cómo. Repasemos rápidamente la arquitectura:

  1. UseCaseService ejecuta el UseCase
  2. El UseCase es envuelto en un UseCaseHandler
  3. El UseCaseHandler es procesado por los Middlewares
  4. Los Middlewares deciden qué hacer con el UseCase

¿Te sientes perdido? Quizás quieras revisar las siguientes entregas de la Newsletter:

Parece que una vez que hemos identificado la cadena de ejecución, podemos realizar los cambios necesarios.

#Actualizando el UseCaseService

export class UseCaseService { constructor( private readonly middlewares: Middleware[], ) {} execute<T extends UseCase>( useCase: UseCase, params?: UseCaseParams<T>, options?: UseCaseOptions, ): Promise<UseCaseReturn<T>> { const requiredOptions = options ?? { logLevel: 'info', } let next = UseCaseHandler.create({ useCase, middleware: new EmptyMiddleware(), options: requiredOptions, }) for (let i = this.middlewares.length - 1; i >= 0; i--) { next = UseCaseHandler.create({ useCase: next, middleware: this.middlewares[i], options: requiredOptions }) } return next.handle(params) as Promise<UseCaseReturn<T>> } }

Hemos añadido a la firma del método un parámetro opcional UseCaseOptions, también le hemos dado un valor por defecto, lo que significa que por defecto siempre registraremos el caso de uso.

Ahora, parece que necesitamos ajustar el UseCaseHandler para que sea capaz de recibir los UseCaseOptions.

#Cambiando el UseCaseHandler

El UseCaseHandler une el UseCase con el Middleware actual que se está aplicando utilizando el patrón decoratorSe abre en una nueva pestaña.

Actualicemos ahora el UseCaseHandler añadiendo las UseCaseOptions:

export class UseCaseHandler implements UseCase { private constructor( readonly useCase: UseCase, private readonly middleware: Middleware, private readonly options: UseCaseOptions, ) {} async handle(params: unknown): Promise<unknown> { return this.middleware.intercept(params, this.useCase, this.options) } static create({ middleware, options, useCase, }: { useCase: UseCase middleware: Middleware options: UseCaseOptions }) { return new UseCaseHandler(useCase, middleware, options) } }

Ahora, solo nos queda que los Middlewares reciban las nuevas opciones.

#Añadiendo las opciones a los Middlewares

Cambiemos primero la definición de middleware:

export interface Middleware { intercept(params: unknown, useCase: UseCase, options: UseCaseOptions): Promise<unknown> }

Y, ahora finalmente, podemos consumir las opciones en el LoggerMiddleware:

export class LoggerMiddleware implements Middleware { async intercept(params: unknown, useCase: UseCase, options: UseCaseOptions): Promise<unknown> { if (options.logLevel === 'silent') { return useCase.handle(params) } const useCaseName = useCase.constructor.name console.log('Logging use case:', useCaseName) console.log('Logging params:', params) console.time(useCaseName) const result = await useCase.handle(params) console.timeEnd(useCaseName) console.log('Logging result:', result) return result } }

Con este último cambio, ahora cuando queramos evitar el registro en consola de un solo caso de uso, podemos hacerlo de la siguiente manera:

useCaseService.execute(loginCmd, { logLevel: 'silent' })

Es una forma limpia, escalable y versátil de reconfigurar la ejecución de los casos de uso a través de middlewares.

#Conclusión

Este sistema me ha permitido en el pasado crear middlewares muy avanzados.

Si necesitara tener un mensaje de confirmación al querer eliminar un recurso en una webapp, usaría un ConfirmMiddleware. Sería tan fácil de activar como lo siguiente:

useCaseService.execute(deleteUserCmd, { confirm: '¿Estás seguro de que quieres eliminar al usuario?' })

¿Te preguntas cómo conectar los UseCases con el código de la UI de una forma desacoplada? Hablaremos en profundidad sobre ello en el futuro. Spoiler alert: con un EventEmitter, que sigue el Patrón de Diseño ObserverSe abre en una nueva pestaña.

Las UseCaseOptions son un patrón inestimable que espero que encuentres útil tú también.

P.D. 1: ¿Te ha resultado útil esta newsletter? ¡Quizás a alguien que conozcas también le resulte útil! ¿Podrías ayudarme compartiéndola con ellos? Aquí tienes el enlace a la Newsletter. ¡Gracias!

P.D. 2: Esta entrega de la newsletter fue escrita en un autobús de Chiang Rai a Chiang Mai, Tailandia.

P.D. 3: Recientemente he empezado una colaboración con JetBrainsSe abre en una nueva pestaña, y estoy sorteando una licencia de 1 año para todos los productos de JetBrains valorada en 299 €. Para participar, tienes que encontrar el Easter Egg en este post donde hablo de cómo maximicé mi productividad en WebStorm después de 10 años usandolo. ¡Mucha suerte!