Wiring it all up to a RESTful API

The cqrs-es library was originally designed for use in serverless environments but since version 0.2.0 works equally well in web servers. Working with an HTTP request is similar in either case.

Note that the following are only suggestions based on experience.

Making changes (commands)

The URL for commands usually follows /{aggregate}/{aggregate_id} with the HTTP body carrying the serialized command payload. Both PUT and POST methods are appropriate, arguments could be made for either since some commands may be idempotent while others are not.

E.g., an UpdateAddress command would likely be idempotent while a WithdrawCash command against the same aggregate would not be.

Example:

curl --request POST 'localhost:3030/account/ACCT-c52a8f04' \
  --header 'Content-Type: application/json' \
  --data-raw '{
      "WriteCheck": {
          "check_number": "1170",
          "amount": 256.28
      }
  }'

With a CQRS framework configured for the aggregate, the request body can then be deserialized and submitted.

fn submit_bank_account_command(&self, aggregate_id: String, request: Request) -> Result<(),Error> {
    let payload: BankAccountCommand = serde_json::from_slice(request.body)?;
    self.cqrs_framework
        .execute(aggregate_id, payload)
        .await?;
    Ok(())
}

Command response

Commands make a change and return no information, if we wish to remain true to the spirit of CQRS this translates directly to an HTTP response of 204 No Content with an empty body.

An empty response isn't always ideal however, particularly if the application is supporting a passive web page for a front end. In this case if the command is successful a query can be immediately made to provide the desired response payload. The cqrs-es framework ensures that all queries are updated before returning successfully

Requesting a materialized view (queries)

Similar to commands, the URL for queries usually follows /{query}/{query_id} where the query id will usually be identical to the id of the aggregate instance. Using a previously configured GenericQuery or ViewRepository this is simple to load, serialize and return.

fn query_bank_account_view(&self, aggregate_id: String) -> Result<String,Error> {
    let account_view: BankAccountView = self.bank_account_query.load(aggregate_id)?;
    Ok(serde_json::to_string(account_view)?)
}