Here is a strongly typed command-bus
TCommand Bus stands for Typed Command Bus
I will use the definitions coming from Matthias Noback series of articles;
Commands are often used in applications that separate the technical aspects of user input from their meaning inside the application. Commands in object-oriented programming are objects, like everything else. A command is literally some kind of an imperative, indicating what behavior a user, or client, expects from the application.
When you use commands to let the user communicate the application's intended behavior, you make the core of your application completely unaware of the world outside. And on top of that, you separate the intended behavior from the actual implementation of that behavior.
The execution will be done thanks to Command Handlers
The command bus eventually calls the command handler which corresponds to the given command object.
The command bus doesn't merely hand over commands to their handlers. Usually it does all kinds of things. For instance a command bus may validate command data, wrap the command handler in a database transaction, provide queueing options for a command, etc.
Usually those things are done thanks to Middleware system. If you're familiar with express, a middleware will be applied before and/or after the handling of your command.
You will have to declare, your Commands
, Commmand Handlers
and Middlewares
Caution:
In TCommand bus, I've made the choice of returning
something from the command bus. It can looks like an anti-pattern BUT often we need it, not everyone can work with fully asynchronous flows and it can be useful
Here is a command
type EditCardLimitsCommand = Command<
{ hasEdited: boolean }, // The return type of your command handling
"editCardLimits", // The name of your command
{ cardId: string; limits: number[] } // The Payload of your command
>;
Here is a command handler
type EditCardLimitsCommandHandler = CommandHandler<EditCardLimitsCommand>;
function buildEditCardLimitsHandler(): EditCardLimitsCommandHandler {
async function handle(
_command: EditCardLimitsCommand
): Promise<{ hasEdited: boolean }> {
// DO WHATEVER YOU LIKE
return {
hasEdited: true,
};
}
return {
handle,
handledCommandKind: "editCardLimits", // is enforced by the type of your command handler
};
}
Here is the middleware
const loggingMiddleware: Middleware = {
name: "loggingMiddleware",
async intercept<TCommand extends CommandType<TCommand>>(
command: TCommand,
next: () => Promise<MiddlewareResult<TCommand>>
): Promise<MiddlewareResult<TCommand>> {
console.log(`Handling ${command.kind} command`);
const result = await nextMiddleware(command, "middlewareOne", next);
console.log(`${command.kind} handled`, { result });
return result;
},
};
Be careful, in TCommand bus, the middlwares will be executed one after the other in the order you put while instanciating the bus
Let's now instantiate the bus!
const editCardLimitsHandler = buildEditCardLimitsHandler();
const handlers = [editCardLimitsHandler];
const middlewares = [loggingMiddleware];
// You will need to pass a logger (can be a dummy one) to the command bus
const logger = {
debug() {},
info() {},
warn() {},
error() {},
};
const bus = buildCommandBus(handlers, middlewares, logger);
As now we have our bus :)
// The type here is important in order to have the return type
const editCardLimitsCommand: EditCardLimitsCommand = {
kind: "editCardLimits",
payload: {
cardId: "123",
limits: [1, 2, 3],
},
};
// THIS IS STRONGLY TYPED! It returns a Result type
const result = await bus.handle(editCardLimitsCommand);