Commands
A command is a message that instructs a service to do some work. It could be a technical instruction, such as BackupDatabase, or modelled on your business, such as PlaceOrder or ShipPackage. This page covers declaring, sending and handling commands.
TIP
Name commands in plain English, as an instruction. It makes it clear what will happen when the command is handled.
Declaring a command
A command is a class that extends Command:
import { Command } from '@node-ts/bus-messages'
export class ChargeCreditCard extends Command {
/**
* A unique name that identifies the message, in a namespace style such as
* organisation/domain/command-name. The bus routes messages by this name.
*/
static NAME = 'my-app/accounts/charge-credit-card'
$name = ChargeCreditCard.NAME
/**
* The contract version of this message. Increment it when the message's
* fields change in a way that isn't backwards compatible.
*/
$version = 1
/**
* Create a charge on a credit card
* @param creditCardToken Identifies the card to charge
* @param amount The amount, in USD, to charge the card
*/
constructor(
readonly creditCardToken: string,
readonly amount: number
) {
super()
}
}Sending a command
A command is handled by a single service, unlike an event, which can have many subscribers. The service usually handles the command, and then publishes an event to say it's done. Send a command with send(), optionally with attributes:
const chargeCreditCard = new ChargeCreditCard('tok_visa', 1200)
// Send a command. It's handled by a single service
await bus.send(chargeCreditCard)
// Send a command with attributes
await bus.send(chargeCreditCard, {
correlationId: 'cd091b26-f0e6-43fb-9962-c06786948e26'
})Handling a command
Commands are handled by a function declared with handlerFor, or a class that implements Handler. Both get the message, its attributes and a HandlerContext. Events published through the context are held until the handler resolves, and dropped if it throws, so a failed command doesn't announce that it succeeded.
export const chargeCreditCardHandler = handlerFor(
ChargeCreditCard,
async (command, _attributes, ctx) => {
await paymentService.charge(command.creditCardToken, command.amount)
await ctx.publish(
new CreditCardCharged(command.creditCardToken, command.amount, new Date())
)
}
)export class ChargeCreditCardHandler implements Handler<ChargeCreditCard> {
// A getter, so the bus can read it without constructing the handler
get messageType() {
return ChargeCreditCard
}
async handle(
command: ChargeCreditCard,
_attributes: MessageAttributes,
ctx: HandlerContext
) {
await paymentService.charge(command.creditCardToken, command.amount)
await ctx.publish(
new CreditCardCharged(command.creditCardToken, command.amount, new Date())
)
}
}Register the handler with the bus configuration, then start the bus to begin handling messages:
const paymentsBus = Bus.configure()
.withMessageTypes(messageTypes)
.withHandler(chargeCreditCardHandler) // Function handler
.build()
await paymentsBus.initialize()
// Start the bus to begin handling messages
await paymentsBus.start()See also
- Events
- Retry strategies, for what happens when a handler throws
- Dependency injection, for class handlers with dependencies