LogoCésar Alberca

Middlewares and The Chain of Responsibility Pattern

2026-01-21T15:00:00.000Z

As we've seen in the previous issue of this newsletter where we talked about the UseCaseService, this service looks like it's terribly easy for it to get out of hand. Let's take a look:

export class UseCaseService { async execute<In, Out>(useCase: UseCase<In, Out>, params?: In): Promise<Out> { try { console.log('Logging use case:', useCase.constructor.name) console.log('Logging params:', params) const result = await useCase.handle(params) console.log('Logging result:', result) return result } catch (e) { alert(e.message) throw e } } }

We have a problem. This class does too many things! It executes the use case, handles the error and logs the result. We are not following the S from SOLID.

If in order to describe what a class does, you use and, then it's doing too many things.

First, once identified the different concerns we can start separating in different classes each concern, so it's more scalable.

#Separating concerns

To separate the concerns of logging and error handling, we are going to introduce the concept of middleware:

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

A middleware is capable of intercepting a request, which basically means doing something with it. Nothing more nothing less. We are defining it as an interface because we are going to have multiple middlewares.

As you might have seen in this architecture series, knowing when to use an interface or an abstract class is quite powerful. It's definitely worth the time to invest in getting to learn when to use them.

Let's see in detail how would the logging middleware look like:

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

Don't use useCase.constructor.name in production as it will probably be mangled by the bundler, which means you'll get in the devtools console stuff like __a, __bc, etc. We'll find something better to log once we tackle the dependency injection container.

There is an interesting piece of code in the middleware. Can you spot it? Yes, we are calling inside the middleware the use case. Wait, what? Well, that's a key aspect of the Chain of ResponsibilityOpen in a new tab. Each Middleware (sometimes referred as Link) calls the next middleware.

But, César, I don't understand; it seems that we are calling the useCase.handle(), which is the method we use for executing the use cases. How can that call the next middleware?

Well, the answer is the decorator design patternOpen in a new tab.

#The Decorator Design Pattern

The decorator pattern allows us to add more functionality to a given class. In our case we want something to call the next middleware and also have in its context the use case we are executing. And that's how the UseCaseHandler comes to exist:

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

With this class we are decorating the UseCase. To do so, we implement the same interface of the element we want to decorate. We also add a static factory method to handle the creation for us. Now, it's only a matter of connecting everything in the UseCaseService.

#Connecting everything in the UseCaseService

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

The UseCaseParams and UseCaseReturn are utility types to get the Params and Return of a use case as a type. They look like this

export type UseCaseParams<T extends UseCase> = T extends UseCase<infer P> ? P : unknown export type UseCaseReturn<T extends UseCase> = T extends UseCase<unknown, infer R> ? R : unknown

Now the UseCaseService receives an array of middlewares ready to be used, each one concerned with its own responsibility. When we execute a use case, we create a chain of middlewares, where each one points to the next one. By default, is always a great practice to have an EmptyMiddleware. What does it do you might ask? Well, nothing, of course!

export class EmptyMiddleware implements Middleware { intercept(params: unknown, useCase: UseCase): Promise<unknown> { return useCase.handle(params) } }

Now, when we create our UseCaseService we can pass our LoggerMiddleware:

const loggerMiddleware = new LoggerMiddleware() const useCaseService = new UseCaseService([loggerMiddleware]) useCaseService.execute(logindCmd)

Let's stop for a second and think about what we have done:

  1. Created a generic way of adding cross-cutting concerns without polluting the use case service.
  2. Made our chain configurable at runtime. Yes, we can slap an if and check if we are in production so we don't add the loggerMiddleware.
  3. Made all our use cases run through a pipeline of middlewares without having to duplicate that logic all over the place.

Pretty nifty.

And the possibilities from here on are endless!

#Conclusion

This is quite a lot to digest. I would suggest you play around with this code so you can better interiorize what we've talked about in this issue. How would you implement the error handling in a middleware? I invite you to try.

P.S: How would you use the middlewares for? Feel free to reply to this email, you might appear in the next issue, where we'll talk about advanced middlewares. Stay tuned!

P.2: I also recently published my yearly review, I kindly invite you to read it if you are interested in Freelancing and Digital Nomadism.