LogoCésar Alberca

Use Case Options for Advanced Middlewares

2026-02-04T15:00:00.000Z

In previous issues of the Newsletter we talked extensively about Use Cases and the tremendous capabilities they bring to your Frontend Architecture.

We've also seen the preferred method of reconfiguring the use cases using Middlewares, however, something is still missing.

Let's imagine that we have a use case that logs sensitive information in the console, and we want to skip it from logging. We still want to keep the rest of the middlewares. How could we go solve this problem?

We could first think of adding an if statement in the 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 } }

This doesn't seem very scalable, right? There must be another way. It feels like we should be able to add context to the execution of use cases. Let's try that!

#Make Use Cases execution context aware

Let's introduce the concept of UseCaseOptions:

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

UseCaseOptions represents an object that has options that are needed by the UseCases, the UseCaseService or the Middlewares.

Next, we have to figure out where to add this information. To do that, I like to think about the API design from the client perspective.

We need to go from this:

useCaseService.execute(loginCmd)

To this:

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

Once we have the what we need to come up with the how. Let's quickly recap the architecture:

  1. UseCaseService executes UseCase
  2. UseCase is wrapped in UseCaseHandler
  3. UseCaseHandler is processed by Middlewares
  4. Middlewares decide what to do with the UseCase

Feeling lost? Perhaps you might want to check the following issues of the Newsletter:

It seems like once we've identified the chain of execution, then we can make the necessary changes.

#Updating the 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>> } }

We've added to the method's signature an optional param UseCaseOptions, we've also given it a default value, which means that by default we'll always log the use case.

Now, it seems we need to tweak the UseCaseHandler so it's able to receive the UseCaseOptions.

#Changing the UseCaseHandler

The UseCaseHandler binds together the UseCase with the current Middleware it's applying using the decorator patternOpen in a new tab.

Let's update now the UseCaseHandler by adding the 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) } }

Now, we need to make the Middlewares aware of the new options!

#Adding the options to the Middlewares

Let's first change the definition of middleware:

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

And, now finally, we can consume the options in the 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 } }

With this last change, now when we want to avoid logging in console a single use case, we can do as follows:

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

It's a clean, scalable and versatile way of reconfiguring use case execution through middlewares.

#Conclusion

This system has allowed me in the past to create very advanced middlewares.

If I needed to have a confirmation message when I want to delete a resource in a webapp, I would use a ConfirmMiddleware. It would be as easy as to trigger as following:

useCaseService.execute(deleteUserCmd, { confirm: 'Are you sure you want to delete the user?' })

Wondering about how to connect UseCases with UI code in a decoupled way? We'll talk in depth about it in the future. Spoiler alert, with an EventEmitter, which follows the Observer Design PatternOpen in a new tab.

UseCaseOptions are an invaluable pattern that I hope you find useful too.

P.S 1: Did you find this newsletter useful? Perhaps somebody you know could find it useful too! Would you kindly help me by sharing it with them? Here's the link to the Newsletter. Thank you!

P.S 2: This newsletter issue was written on a bus from Chiang Rai to Chiang Mai, Thailand.

P.S 3: I recently started a collaboration with JetBrainsOpen in a new tab, and I'm giving away a 1-year license for all JetBrains products valued at 299 €. To participate, you need to find the Easter egg in this post that I talk about how I maximized my productivity in Webstorm after 10 years of using it. Good luck!