Skip to main content

Command

Commands are any action a user performs on your application. For example, RemoveItemFromCart, RatePhoto or AddCommentToPost. They express the intention of an user, and they are the main interaction mechanism of your application. They are a similar to the concept of a request on a REST API. Command issuers can also send data on a command as parameters.

Creating a command

The Booster CLI will help you to create new commands. You just need to run the following command and the CLI will generate all the boilerplate for you:

boost new:command CreateProduct --fields sku:SKU displayName:string description:string price:Money

This will generate a new file called create-product in the src/commands directory. You can also create the file manually, but you will need to create the class and decorate it, so we recommend using the CLI.

Declaring a command

In Booster you define them as TypeScript classes decorated with the @Command decorator. The Command parameters will be declared as properties of the class.

src/commands/command-name.ts
@Command()
export class CommandName {
public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType) {}
}

These commands are handled by Command Handlers, the same way a REST Controller do with a request. To create a Command handler of a specific Command, you must declare a handle class function inside the corresponding command you want to handle. For example:

src/commands/command-name.ts
@Command()
export class CommandName {
public constructor(readonly fieldA: SomeType, readonly fieldB: SomeOtherType) {}

public static async handle(command: CommandName, register: Register): Promise<void> {
// Validate inputs
// Run domain logic
// register.events([event1,...])
}
}

Booster will then generate the GraphQL mutation for the corresponding command, and the infrastructure to handle them. You only have to define the class and the handler function. Commands are part of the public API, so you can define authorization policies for them, you can read more about this on the authorization section.

tip

We recommend using command handlers to validate input data before registering events into the event store because they are immutable once there.

The command handler function

Each command class must have a method called handle. This function is the command handler, and it will be called by the framework every time one instance of this command is submitted. Inside the handler you can run validations, return errors, query entities to make decisions, and register relevant domain events.

Registering events

Within the command handler execution, it is possible to register domain events. The command handler function receives the register argument, so within the handler, it is possible to call register.events(...) with a list of events.

src/commands/create-product.ts
@Command()
export class CreateProduct {
public constructor(readonly sku: string, readonly price: number) {}

public static async handle(command: CreateProduct, register: Register): Promise<string> {
register.event(new ProductCreated(/*...*/))
}
}

For more details about events and the register parameter, see the Events section.

Returning a value

The command handler function can return a value. This value will be the response of the GraphQL mutation. By default, the command handler function expects you to return a void as a return type. Since GrahpQL does not have a void type, the command handler function returns true when called through the GraphQL. This is because the GraphQL specification requires a response, and true is the most appropriate value to represent a successful execution with no return value.

If you want to return a value, you can change the return type of the handler function. For example, if you want to return a string:

For example:

src/commands/create-product.ts
@Command()
export class CreateProduct {
public constructor(readonly sku: string, readonly price: number) {}

public static async handle(command: CreateProduct, register: Register): Promise<string> {
register.event(new ProductCreated(/*...*/))
return 'Product created!'
}
}

Validating data

tip

Booster uses the typed nature of GraphQL to ensure that types are correct before reaching the handler, so you don't have to validate types.

Throw an error

A command will fail if there is an uncaught error during its handling. When a command fails, Booster will return a detailed error response with the message of the thrown error. This is useful for debugging, but it is also a security feature. Booster will never return an error stack trace to the client, so you don't have to worry about exposing internal implementation details.

One case where you might want to throw an error is when the command is invalid because it breaks a business rule. For example, if the command contains a negative price. In that case, you can throw an error in the handler. Booster will use the error's message as the response to make it descriptive. For example, given this command:

src/commands/create-product.ts
@Command()
export class CreateProduct {
public constructor(readonly sku: string, readonly price: number) {}

public static async handle(command: CreateProduct, register: Register): Promise<void> {
const priceLimit = 10
if (command.price >= priceLimit) {
throw new Error(`price must be below ${priceLimit}, and it was ${command.price}`)
}
}
}

You'll get something like this response:

{
"errors": [
{
"message": "price must be below 10, and it was 19.99",
"path": ["CreateProduct"]
}
]
}

Register error events

There could be situations in which you want to register an event representing an error. For example, when moving items with insufficient stock from one location to another:

src/commands/move-stock.ts
@Command()
export class MoveStock {
public constructor(
readonly productID: string,
readonly origin: string,
readonly destination: string,
readonly quantity: number
) {}

public static async handle(command: MoveStock, register: Register): Promise<void> {
if (!command.enoughStock(command.productID, command.origin, command.quantity)) {
register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))
} else {
register.events(new StockMoved(/*...*/))
}
}

private enoughStock(productID: string, origin: string, quantity: number): boolean {
/* ... */
}
}

In this case, the command operation can still be completed. An event handler will take care of that `ErrorEvent and proceed accordingly.

Reading entities

Event handlers are a good place to make decisions and, to make better decisions, you need information. The Booster.entity function allows you to inspect the application state. This function receives two arguments, the Entity's name to fetch and the entityID. Here is an example of fetching an entity called Stock:

src/commands/move-stock.ts
@Command()
export class MoveStock {
public constructor(
readonly productID: string,
readonly origin: string,
readonly destination: string,
readonly quantity: number
) {}

public static async handle(command: MoveStock, register: Register): Promise<void> {
const stock = await Booster.entity(Stock, command.productID)
if (!command.enoughStock(command.origin, command.quantity, stock)) {
register.events(new ErrorEvent(`There is not enough stock for ${command.productID} at ${command.origin}`))
}
}

private enoughStock(origin: string, quantity: number, stock?: Stock): boolean {
const count = stock?.countByLocation[origin]
return !!count && count >= quantity
}
}

Authorizing a command

Commands are part of the public API of a Booster application, so you can define who is authorized to submit them. All commands are protected by default, which means that no one can submit them. In order to allow users to submit a command, you must explicitly authorize them. You can use the authorize field of the @Command decorator to specify the authorization rule.

src/commands/create-product.ts
@Command({
authorize: 'all',
})
export class CreateProduct {
public constructor(
readonly sku: Sku,
readonly displayName: string,
readonly description: string,
readonly price: number
) {}

public static async handle(command: CreateProduct, register: Register): Promise<void> {
register.events(/* YOUR EVENT HERE */)
}
}

You can read more about this on the Authorization section.

Submitting a command

Booster commands are accessible to the outside world as GraphQL mutations. GrahpQL fits very well with Booster's CQRS approach because it has two kinds of operations: Mutations and Queries. Mutations are actions that modify the server-side data, just like commands.

Booster automatically creates one mutation per command. The framework infers the mutation input type from the command fields. Given this CreateProduct command:

@Command({
authorize: 'all',
})
export class CreateProduct {
public constructor(
readonly sku: Sku,
readonly displayName: string,
readonly description: string,
readonly price: number
) {}

public static async handle(command: CreateProduct, register: Register): Promise<void> {
register.events(/* YOUR EVENT HERE */)
}
}

Booster generates the following GraphQL mutation:

mutation CreateProduct($input: CreateProductInput!): Boolean

where the schema for CreateProductInput is

{
sku: String
displayName: String
description: String
price: Float
}

Commands naming convention

Semantics are very important in Booster as it will play an essential role in designing a coherent system. Your application should reflect your domain concepts, and commands are not an exception. Although you can name commands in any way you want, we strongly recommend you to name them starting with verbs in imperative plus the object being affected. If we were designing an e-commerce application, some commands would be:

  • CreateProduct
  • DeleteProduct
  • UpdateProduct
  • ChangeCartItems
  • ConfirmPayment
  • MoveStock
  • UpdateCartShippingAddress

Despite you can place commands, and other Booster files, in any directory, we strongly recommend you to put them in <project-root>/src/commands. Having all the commands in one place will help you to understand your application's capabilities at a glance.

<project-root>
├── src
│   ├── commands <------ put them here
│   ├── common
│   ├── config
│   ├── entities
│   ├── events
│   ├── index.ts
│   └── read-models