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)?)
}