Middlewares
Missive.js includes a powerful middleware system in each bus to process intents (command
, query
, and event
) in a flexible and extendable manner.
Middleware allows you to insert cross-cutting concerns such as validation, logging, authentication, error handling, and more into the
processing pipeline without duplicating code across individual handlers. This makes it easy to maintain and scale your application while keeping
business logic clean and separated.
How middleware works
Middleware in Missive.js works similarly to middleware in other JavaScript frameworks like Express, Koa or Fastify. Each middleware function wraps around the processing of an intent, giving you control over the flow and transformation of the intent as it moves through the service bus.
Bus Missive.js has its concept of Envelope and Stamps, which are used to pass data and metadata between middlewares and handlers, allowing you to inject additional information or context into the processing pipeline.
Here’s how it works:
-
Intent Dispatch
When an intent is dispatched to the service bus, it is immediately wrapped in an Envelope.
-
Middleware Execution
The Envelope passes through each middleware sequentially. Each middleware function receives the intent wrapped in the Envelope and a reference to the next function, which calls the next middleware or the handler in the chain.
Middleware can, for instance, do the following:
- Inspect the intent and its Envelope.
- Transform or add Stamps to the intent or its Envelope.
- Validate the intent’s data or context before it reaches the handler.
- Log the intent’s details for monitoring or auditing purposes.
- Handle errors by catching exceptions from downstream middlewares or the handler.
There is no limit:
- to the number of middlewares you can add to a bus, allowing you to create complex processing pipelines with ease.
- to what you can do with a middleware
-
Handler Execution
After the intent passes through all the middleware, it reaches the handler defined for the specific intent type. The handler processes the intent based on its business logic and Stamps
handled
is added to the envelope.
Creating Middleware
Middleware functions in Missive.js are simple, functions that take in two parameters:
- an Intent wrapped in the Envelope: the current intent being processed.
- next: A function that calls the next middleware or handler in the pipeline.
Here’s an example middleware function:
async (envelope, next) => { console.log('Logger Middleware: Message Received', envelope.message); await next(); console.log('Logger Middleware: Message Handled', envelope.message);}
Using Middleware
To use middleware in Missive.js, you attach it to the service bus when configuring it. Middleware functions are applied in the order they are added, forming a processing pipeline.
const myMiddleware = createMyMiddleware<'query', QueryHandlerRegistry>();const queryBus = createQueryBus<QueryHandlerRegistry>();queryBus.use(loggerMiddleware);
You can also use the built-in middleware in a simpler way
const queryBus: QueryBus = createQueryBus<QueryHandlerRegistry>();queryBus.useLoggerMiddleware();queryBus.useCacherMiddleware();queryBus.useLockMiddleware({ getLockKey: (envelope) => envelope.message.id });queryBus.use(myMiddleware);
Best Practices
- Order Matters: Middlewares are executed in the order they are registered. Place essential middleware, early in the chain to prevent invalid or unauthorized intents from reaching the handler.
- Keep Middleware Focused: Each middleware should handle one concern to keep the pipeline clean and maintainable.
- Mutation: Avoid mutating the intent or its Envelope in a middleware unless necessary. Instead, use Stamps to pass data between middlewares and handlers.
Summary
Middleware in Missive.js offers a powerful and flexible way to manage the flow of intents through your system. By inserting middleware into the pipeline, you can centralize cross-cutting concerns like validation, logging, and error handling, ensuring that your codebase remains modular, clean, and easy to scale.
With these principles in mind, you can build a robust, maintainable architecture that allows for complex scenarios like retry policies, detailed logging, and consistent validation across your entire application.
Built-in Middlewares are intent
aware
Every single built-in middleware can be configured to be intent
aware. This means that you can have a different configuration for each intent type.
const queryBus: QueryBus = createQueryBus<QueryHandlerRegistry>();queryBus.useCacherMiddleware({ ...defaultOptions intents: { YouQueryCommandEventType: { ...specificOptions }, },})
Breaking the Chain (of middlewares) advanced
In some situations, you may want to stop the processing of an intent before it reaches the handler. Indeed, if a middleware function does not call the next
function,
the chain middleware will be broken, and the intent will not be processed further.
This is really convenient for performance for instance, but it is not always what you want to do.
For this reason the bus will never handle an intent that has been handled already. It does that information thanks to the missive:handled
stamp.
Within the built-in middlewares, here is the list of the ones where you have the option to break the chain. (default: yes):
- CacherMiddleware: if the intent has been cached.
- FeatureFlagMiddleware: if the intent has been handled by a fallbackHandler.
- AsyncMiddleware: when the envelope is sent to a queue.
Missive.js. MIT License.
Powered by Astro Starlight.
Inspired by Symfony Messenger