Messages
Messages are small pieces of data passed between services. A message is either an instruction to do something, or a report that something has happened. This page covers what the kinds of message are and the two ways to declare them.
A message is sent or published by one service and received by any number of others, depending on its kind:
ChargeCreditCard. Sent, and handled by one service.EventsA fact about something that happened, such as CreditCardCharged. Published to every subscriber.System messagesMessages from other systems, such as S3 notifications, that don't follow the bus' conventions.Declaring messages
A message is a class that extends Command or Event from @node-ts/bus-messages. It has:
- a static
NAME, and a$nameset to it. The bus routes messages by this name, so make it unique, in a namespace style such asmy-app/accounts/charge-credit-card. - a
$version, the version of its contract. Increment it when its fields change in a way that isn't backwards compatible.
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()
}
}The bus reads the static NAME without constructing the class, so constructors can take arguments. A subclass needs its own NAME, or the bus rejects it.
TIP
Declare your messages in a package of their own that the services which send and receive them share. Generate their message types in that package, and export them from its entry, so every service can pass them to its bus.
Without a class
A message that's only data can be declared with defineCommand or defineEvent instead. Give the name first and the type of its fields second. The definition is a function that creates the message, and it can be used anywhere a message class can.
export const RefundPayment = defineCommand('my-app/accounts/refund-payment')<{
paymentId: string
refundedAt: Date
}>()
export type RefundPayment = MessageOf<typeof RefundPayment>
// The contract version defaults to 0
export const PaymentRefunded = defineEvent('my-app/accounts/payment-refunded', {
version: 1
})<{ paymentId: string }>()
export type PaymentRefunded = MessageOf<typeof PaymentRefunded>export const refundPaymentHandler = handlerFor(
RefundPayment,
async (command, _attributes, ctx) => {
await ctx.publish(PaymentRefunded({ paymentId: command.paymentId }))
}
)Messages declared this way are plain objects, both when they're created and when they're received, so they have no prototype, instanceof or methods. Both styles can be mixed, and look the same on the wire.
Constructors aren't run on receipt
A received message is created from its class' prototype, and its fields are copied onto it. Don't rely on constructor logic or field initializers in message classes: a field missing from the payload stays undefined.
See also
- Message attributes, for metadata that travels with a message
- Serializers, for how Dates and classes in messages are restored
defineCommandandCommandin the API reference