NAV Navbar

A bird's eye view of Booster

Booster was synthesized from years of experience in high availability, and high performance software scenarios. Most implementation details in these situations repeat time after time. Booster abstracts them in order for you to focus on what matters: The domain of your application.

In this section you'll get a small grasp of how Booster works, but this is not a full guide.

Just get a taste of Booster, and when you're ready, you can install Booster, begin writing your first Booster app, or even get into the in-depth reference documentation.

Think about user actions, not endpoints

@Command({
  authorize: 'all',
})
export class SendMessage {
  public constructor(
    readonly chatroomID: UUID,
    readonly messageContents: string
  ) {}

  public handle(register) {
    const timestamp = new Date()
    register.events(
      new MessageSent(this.chatroomID, this.messageContents, timestamp)
    )
  }
}

A user action is modeled in Booster as a Command.

Similar to controllers, command handlers serve as one of the entry points to your system, they scale horizontally automatically.

Commands are defined as decorated TypeScript classes with some fields and a handle method.

Time travel through your data

@Event
export class MessageSent {
  public constructor(
    readonly chatroomID: UUID,
    readonly messageContents: string,
    readonly timestamp: Date
  ) {}

  public entityID(): UUID {
    return this.chatroomID
  }
}

Instead of mutating your data in a database, Booster stores an infinite stream of events. You get the possibility of seeing how your data changes through time and space.

Need to fix a bug that happened one year ago? Just change the event generation and re-run it from the past.

Events, like Commands, are just TypeScript classes. No strings attached.

Data modelling

interface Message {
  contents: string
  hash: string
}

@Entity
export class Chatroom {
  public constructor(
    readonly id: UUID,
    readonly messages: Array<Message>,
    readonly lastActivity: Date
  ) {}

  @Reduces(MessageSent)
  public static reduceMessageSent(event: MessageSent, prev?: Chatroom): Chatroom {
    const message = {
      contents: event.messageContents,
      hash: md5sum.digest(event.messageContents)
    }

    if (prev) {
      prev.messages.push(message)
      prev.lastActivity =
        event.timestamp > prev.lastActivity
        ? event.timestamp
        : prev.lastActivity
      return prev
    }

    return new Chatroom(event.chatroomID, [message], event.timestamp)
  }
}

Define your data with TypeScript types, without having to learn a new data definition language.

Entities are the central part of your domain. They are a representation of your event stream at some point in time.

You specify the fields of your entity as all the important things that will be generated from your events. Then you change those fields as events occur.

No alien libraries, no need to think about state of the database, just plain TypeScript and some cool decorators.

On top of that, Entities serve as automatic snapshots, so your app is very fast!

Combining and transforming your data

@ReadModel
export class ChatroomActivity {
  public constructor(
    readonly id: UUID,
    readonly lastActivity: Date,
  ) {}

  @Projects(Chatroom, 'id')
  public static updateWithChatroom(chatroom: Chatroom, prev?: ChatroomActivity): ChatroomActivity {
    return new ChatroomActivity(chatroom.id, chatroom.lastActivity)
  }
}

Most of the time, you don't want to expose all the data you are storing in your system. You might want to hide some parts, transform others.

Also, you might want to combine some entities into one object, so the client can read them more efficiently.

Read models allow you to do all that. With them you can get your data delivered in the shape that you want, instantly to your client. Booster will push the changes live, so you only have to focus on consuming it in the way you require.

GraphQL is hard? Who said that?

# Send a command
mutation {
  SendMessage(
    input: {
      chatroomID: 1,
      messageContents: "Hello Booster!"
    }
  )
}

# Subscribe to a read model
subscription {
  ChatroomActivity(id: 1) {
    id
    lastActivity
  }
}

GraphQL is nice on the client side, but on the backend, it requires you to do quite some work. Defining resolvers, schema, operations, and friends take some time, and it is not the most thrilling work you can do. Especially when your domain has nothing to do with managing a GraphQL API.

Each Command is mapped to a GraphQL mutation, and each ReadModel is mapped to a GraphQL query or subscription.

Just write your Booster app as you would do normally, and enjoy a GraphQL API for free, with its schema, operations, and everything.

Fasten your seat belts

This is a simplified view of Booster. It supports many other features that will definitely speed-up your development process. Among them:

All of this under the best practices of security and privacy of your cloud provider. Booster defaults to the strictest option, so you don't have to worry about security configuration beforehand.

Thrilled already? Jump to the installation steps, read how to write your first Booster app, and join the community in the Spectrum chat.

Installing Booster

Supported operating systems

You can develop with Booster using any of the following operating systems:

Booster hasn't been tested under other platforms like BSD, if you want to develop under those, proceed at your own risk!

Install Node.js

# Ubuntu
$ curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
$ sudo apt install nodejs

# MacOS
$ brew install node

# Windows
> choco install nodejs

Booster is a TypeScript framework that benefits from the Node.js ecosystem, it has been tested under versions newer than v12, so make sure that you install one accordingly.

If you don't have Node.js installed, you can download an installer from it's website, or you can install it using your system's package manager.

If for some reason you are working with other projects that require a different Node.js version, we recommend that you use a version manager like:

Verify your Node.js and npm versions

$ node -v
v13.12.0

$ npm -v
6.14.4

After you've installed Node.js, you can verify that it was installed properly by checking so from your terminal.

Make sure that Node.js is newer than v12 and npm (comes installed with Node.js) is newer than 6.

Set up your AWS account

Booster is a cloud-native framework, meaning that your application will be deployed to the cloud, using different cloud services. Right now, it only supports AWS, but given Booster's abstractions, a provider package can be easily created to support other cloud providers.

To follow the documentation locally and get a grip of Booster, you don't need a cloud provider, but to deploy, and test your application, you will need it.

Now it is a good time to create that AWS account, you can do so from the AWS console registration.

Once you've registered yourself, you will need to configure your system to use your account. To do so, login into the AWS Console, and click on your account name on the top-right corner.

aws account menu location

A menu will open, click on My security credentials and it will take you to the Identity and Access Management panel. Once there, create an access key:

create access key button location

A pop-up will appear, don't close it!.

[default]
aws_access_key_id = <YOUR ACCESS KEY ID>
aws_secret_access_key = <YOUR SECRET ACCESS KEY>

Now create a folder called .aws under your home folder, and a file called credentials inside of it.

Paste the template you see on the right, and fill with the keys that appeared in the popup of the website. Save the file. You are ready to go!

Installing the Booster CLI

Booster comes with a command line tool that generates boilerplate code, and also, deploys, and deletes your application resources in the cloud.

Installing using npm

npm install --global @boostercloud/cli

All stable versions are published to npm, to install the Booster CLI, use the command on the right.

These versions are the recommended ones, as they are well documented, and the changes are stated in the release notes.

Installing the development version

If you like to live on the bleeding edge, you might want to install the development version, but beware, here might be bugs and unstable features!

# Inside a terminal
$ npm install -g verdaccio

# Open a new terminal, and run this command
$ verdaccio

# Go back to the first terminal
$ npm adduser --registry http://localhost:4873
$ git clone git@github.com:boostercloud/booster.git
$ cd booster
$ lerna publish --registry http://localhost:4873 --no-git-tag-version --canary
# Specify some version that you will remember here, i.e. 0.3.0-my-alpha
$ git stash -u
$ npm install --registry http://localhost:4873 @boostercloud/cli

Make sure that you have Git installed. You can verify this by running git help.

Follow the steps on the right, they will:

If everything went correctly, you should have the Booster CLI installed.

Verify that you have Booster installed

To verify that the Booster installation was successful, enter the following command into your terminal: boost version

If everything went well, you should get something like @boostercloud/cli/0.3.0

You are now ready to write your first Booster app!

Your first Booster app in 10 minutes

In this section, we will go through all the necessary steps to have the backend for a blog application up and running in a few minutes.

The steps to follow will be:

1. Create project

Create project

boost new:project <project-name>

First of all, we need to create a base Booster project. To do so, we will use the Booster CLI, which can be invoked by typing boost inside of a terminal.

|- <your-project-name>
  |- src
    |- commands
    |- common
    |- entities
    |- events
    |- read-models
    ...

It will generate a folder with your selected project name.

You will need to answer a few questions in order to configure the project. The last step asks you about a provider package, for this tutorial, select AWS.

Once the project has been successfully created, you will need to move to the new directory, you can do so by typing cd <your project name> in a terminal.

Now open the project in your most preferred IDE, e.g. Visual Studio Code.

2. First command

We will now define our first command, which will allow us to create posts in our blog.

New command

boost new:command CreatePost --fields postId:UUID title:string content:string author:string

In a terminal, from the root of your project, type:

|- <your-project-name>
  |- src
    |- commands/CreatePost.ts

These commands creates most of the code for us, which can be seen in

However, we still need to define a couple of things in this file:

For the first part, we will let anyone to trigger it. To do so, configure the authorize command option to "all" (yes, between quotes, it is a string). If you cannot find it, it is right after the @Command decorator.

Additionally, the current CreatePost command will not trigger any event, so we will have to come back later to set the event that this command will fire up. This is done in the handle method of the command class. Leave it as it is for now.

CreatePost command

@Command({
  authorize: 'all'// Specify authorized roles here. Use 'all' to authorize anyone
})
export class CreatePost {
  public constructor(
    readonly postId: UUID,
    readonly title: string,
    readonly content: string,
    readonly author: string,
  ) {}

  public handle(register: Register): void {
    register.events( /* YOUR EVENT HERE */)
  }
}

If everything went well, you should have now the code you can see on the right.

3. First event

New event

boost new:event PostCreated --fields postId:UUID title:string content:string author:string

In this type of backend architectures, events can be triggered by commands or by other events. We will create an event that defines a Post creation.

|- <your-project-name>
  |- src
    |- events/PostCreated.ts

You will realize that a new file has been created:

Define entity id

public entityID(): UUID {
  return this.postId
}

There is one small thing that we have to define in the above file, which is the returned value for EntityID(). We will set the post UUID. It should look like this:

PostCreated event

@Event
export class PostCreated {
  public constructor(
    readonly postId: UUID,
    readonly title: string,
    readonly content: string,
    readonly author: string,
  ) {}

  public entityID(): UUID {
    return this.postId
  }
}

Your event should look like this:

Add event to CreatePost Command

public handle(register: Register): void {
  register.events(new PostCreated(this.postId, this.title, this.content, this.author))
}

Now we can go back to the command we created before and add our new event PostCreated to the register of events. Your handle should look like this:

4. First entity

New entity

boost new:entity Post --fields title:string content:string author:string --reduces PostCreated

We have now created a command and an event, however, we do not have any data representation of a Post. As a result, we will create an entity.

|- <your-project-name>
  |- src
    |- entities/Post.ts

Another file has been created in your project, you will need to add the implementation of its reduction:

Reduction

@Reduces(PostCreated)
public static reducePostCreated(event: PostCreated, currentPost?: Post): Post {
return new Post(event.postId, event.title, event.content, event.author)
}

In the future, we may want to project events for this Post entity that require retrieving current Post values. In that case we would need to make use of currentPost argument.

Post entity

@Entity
export class Post {
  public constructor(
    public id: UUID,
    readonly title: string,
    readonly content: string,
    readonly author: string,
  ) {}

  @Reduces(PostCreated)
  public static reducePostCreated(event: PostCreated, currentPost?: Post): Post {
    return new Post(event.postId, event.title, event.content, event.author)
  }
}

The full code for the entity can be seen on the right.

5. First read model

New read model

boost new:read-model PostReadModel --fields title:string content:string author:string --projects Post:id

Almost everything is set-up. We just need to provide a way to view the Posts of our blog. For that, we will create a read model.

|- <your-project-name>
  |- src
    |- read-models/PostReadModel.ts

Once the read-model code has been generated, we will also need to define a few things:

@ReadModel({
  authorize: 'all'// Specify authorized roles here. Use 'all' to authorize anyone
})

To make it easy, we will allow anyone to read it:

@Projects(Post, "id")
public static projectPost(entity: Post, currentPostReadModel?: PostReadModel): PostReadModel {
  return new PostReadModel(entity.id, entity.title, entity.content, entity.author)
}

and we will project the whole entity

PostReadModel read model

@ReadModel({
  authorize: 'all'// Specify authorized roles here. Use 'all' to authorize anyone
})
export class PostReadModel {
  public constructor(
    public id: UUID,
    readonly title: string,
    readonly content: string,
    readonly author: string,
  ) {}

  @Projects(Post, "id")
  public static projectPost(entity: Post, currentPostReadModel?: PostReadModel): PostReadModel {
    return new PostReadModel(entity.id, entity.title, entity.content, entity.author)
  }
}

The read model should look like the code on the right:

6. Deployment

boost deploy -e production

Everything we need for a basic project is set. It is time to deploy it:

It will take a couple of minutes to deploy all the resources.

Example GraphQL endpoint

https://<some random number>.execute-api.us-east-1.amazonaws.com/production/graphql

When the the serverless backend is successfully deployed you will see information about your stack endpoints. For this basic project we will only need to pick the REST API endpoint, reflected in the output as backend-application-stack.baseRESTURL, and append /graphql at the end, e.g.:

We will use this GraphQL API endpoint to test our backend.

7. Testing

Let's get started testing the project. We will perform three actions:

To perform the GraphQL queries, you might want to use something like Postwoman, although curl would also work.

7.1 Creating posts

The first GraphQL mutation:

mutation { 
    CreatePost(input: {
        postId: "95ddb544-4a60-439f-a0e4-c57e806f2f6e",
        title: "This is my first post",
        content: "I am so excited to write my first post",
        author: "Some developer"
    })
}

The second GraphQL mutation:

mutation { 
    CreatePost(input: {
        postId: "05670e55-fd31-490e-b585-3a0096db0412",
        title: "This is my second post",
        content: "I am so excited to write my second post",
        author: "The other developer"
    })
}

We will perform two GraphQL mutation queries in order to add information:

The expected response for each of the requests above should be:

{
  "data": {
    "CreatePost": true
  }
}

We should now have two Posts in our backend, no authorization header is required since we have allowed all access to our commands and read models.

GraphQL query, all posts

query {
  PostReadModels {
      id
      title
      content
      author
  }
}

7.2 Retrieving all posts

In order to retrieve the information we just sent, lets perform a GraphQL query that will be hitting our read model PostReadModel:

Query all posts response

{
  "data": {
    "PostReadModels": [
      {
        "id": "05670e55-fd31-490e-b585-3a0096db0412",
        "title": "This is my second post",
        "content": "I am so excited to write my second post",
        "author": "The other developer"
      },
      {
        "id": "95ddb544-4a60-439f-a0e4-c57e806f2f6e",
        "title": "This is my first post",
        "content": "I am so excited to write my first post",
        "author": "Some developer"
      }
    ]
  }
}

You should expect a response similar to this:

GraphQL query, specific posts

query {
  PostReadModel(id: "95ddb544-4a60-439f-a0e4-c57e806f2f6e") {
      id
      title
      content
      author
  }
}

7.3 Retrieving specific post

It is also possible to retrieve specific a Post by adding the id as input, e.g.:

Query specific posts response

{
  "data": {
    "PostReadModel": {
      "id": "95ddb544-4a60-439f-a0e4-c57e806f2f6e",
      "title": "This is my first post",
      "content": "I am so excited to write my first post",
      "author": "Some developer"
    }
  }
}

You should expect a response similar to this:

8. Removing stack

Now, let's undeploy our backend.

Undeploy stack

boost nuke -e production

To do so, execute the following command from the root of your project, in a terminal:

It will ask you to verify the project name, it will be the same one that it was written when we created the project. If you don't remember the name, go to config/production.ts and copy the name field.

9. More functionalities

The are many other options for your serverless backend built with Booster Framework:

But we won't be covering them in this section. Keep reading if you want to know more!

Configuration and environments

One of the goals of booster is to avoid configuration parameters as much as possible, inferring everything automatically from your code. However, there are some aspects that can't be inferred (like the application name), or it is useful to change them under certain circumstances, especially when using different environments

Booster configuration

import { Booster } from '@boostercloud/framework-core'
import { BoosterConfig } from '@boostercloud/framework-types'
import { Provider } from '@boostercloud/framework-provider-aws'

Booster.configure('pre-production', (config: BoosterConfig): void => {
  config.appName = 'my-app-name'
  config.provider = Provider
})

You configure your application by doing a call to the Booster.configure() method. There are no restrictions about where you should do this call, but the convention is to do it in your configuration files located in the src/config folder. If you used the project generator (boost new:project <project-name>), this is where the config files are by default.

The following is the list of the fields you can configure:

Environments

You can configure multiple environments calling to the Booster.configure function several times using different environment names:

import { Booster } from '@boostercloud/framework-core'
import { BoosterConfig } from '@boostercloud/framework-types'
// A provider that deploys your app to AWS:
import { AWSProvider } from '@boostercloud/framework-provider-aws'
// A provider that deploys your app locally:
import { LocalProvider } from '@boostercloud/framework-provider-local' 

Booster.configure('dev', (config: BoosterConfig): void => {
  config.appName = 'fruit-store-dev'
  config.provider = LocalProvider
})

Booster.configure('stage', (config: BoosterConfig): void => {
  config.appName = 'fruit-store-stage'
  config.provider = AWSProvider
})

Booster.configure('prod', (config: BoosterConfig): void => {
  config.appName = 'fruit-store-prod'
  config.provider = AWSProvider
})

// This other configuration could be in another file that Pepe doesn't commit
Booster.configure('pepe', (config: BoosterConfig): void => {
  config.appName = 'pepe-fruit-store'
  config.provider = AWSProvider
})

The environment name will be required by any command from the Booster CLI that depends on the provider. For instance, when you deploy your application, you'll need to specify which environment you want to deploy.

This way, you can have different configurations depending on your needs.

For example, your 'fruit-store' app can have three environments: 'dev', 'stage', and 'prod', each of them with different app names or providers. A developer named "Pepe" could have another environment with a different app name so that he can deploy the entire application to a production-like environment and test it. Check the example to see how this app would be configured.

Architecture and core concepts

Booster’s architecture is heavily inspired by the CQRS and Event Sourcing patterns. These patterns have proven to work well for highly-distributed high available systems, being a tool to make resilient software that is fast and scales very well, especially in distributed scenarios.

The Booster high-level architecture diagram looks like this: Booster architecture

With these patterns combined, in a Booster Application:

This architecture has many advantages:

It's usually non-trivial to get event-driven architecture design right and implement a maintainable event-driven solution that scales, but Booster has been built around these concepts and will greatly help you and your team to keep things under control. Booster integrates event-driven design in a way that simplifies their usage and understanding.

Commands and Command Handlers - The Write Pipeline

You can create a command manually or using the generator provided with the boost CLI tool. Let's create a command to confirm a payment:

boost new:command ConfirmPayment --fields cartID:UUID confirmationToken:string

You can specify as many fields as you want, and Booster will generate a class for you in the src/commands folder that more or less will look like this:

@Command({
  authorize: 'all',
})
export class ConfirmPayment {
  public constructor(readonly cartID: UUID, readonly confirmationToken: string) {}

  public handle(register: Register): void {
    // The `register` parameter injected can be used to register any number of events.
    register.events(new CartPaid(this.cartId, this.confirmationToken))
  }
}

Commands and Command Handlers define the write API of your application (highlighted in yellow in the diagram). Commands are objects that are sent to the /commands endpoint. The usage of this endpoint is explained in the REST API section.

Similarly to controllers in a traditional MVC architecture, commands are synchronously dispatched by a handler method, which will be in charge of validating the input and registering one or more events in the system. While command handlers can run arbitrary code, it is recommended to keep them small, focusing on data acceptance and delegating as much logic to event handlers.

A command is a class, decorated with the @Command decorator, that defines a data structure and a handle method. The method will process the commands and optionally generate and persist one or more events to the event store.

Note how no magic happened in the generator. The only thing that required for Booster to know that this class is a command, is the @Command decorator. You could get the same result by writing the class yourself 😉

Events

To create an event class, you can do the same thing that you did with a command, either manually, or with the generator, using the boost command line tool:

  boost new:event <name of the event> --fields fieldName:fieldType

Booster will generate a class for you in the src/events folder:

  @Event
  export class CartPaid {
    public constructor(readonly cartID: UUID, readonly confirmationToken: string) {}

    public entityID(): UUID {
      return this.cartId
    }
  }

An event is a data structure that represents a fact and is the source of truth for your application. Instead of mutating your database, you store an event representing that mutation. Think of your bank account, instead of storing your balance in some database table, mutating the value every time you perform an operation, it stores events for each of them. The balance is then calculated on the fly and shown to you any time you request it. Two examples of events in your bank account would be:

You can define as many event handler classes as you want to react to them. For example, imagine that a specific event represents that your account has reached zero. You can write a handler to notify the user by email. In a Booster application, it is recommended to write most your domain logic in event handlers.

Notice the required entityID method. All events are grouped by their event type and the value returned by entityID. All events are somehow tied to a concept in your domain model, in our bank account example, this could be the account number.

In the previous example, the CartPaid event has a cartID field, which then you will return in the entityID method. This allows booster to find this event when the system requests to build the state of a specific Cart.

In most situations your event stream will be reduced to a domain model object, like that Cart (An Entity), but there are some use cases on which the event stream is just related to a specific entity, for example, a register of sensor values in a weather station, which are related to the station, but the station has no specific value that needs to be reduced. You can implement the semantics that best suit your needs.

Event Handlers

@EventHandler(CartPaid)
export class CartPaidHandler {
  public static handle(event: CartPaid, register: Register) {
    register.events(new OrderPreparationStarted(event.cartID))
  }
}

You can react to events implementing an Event Handler class. An Event Handler is a regular class that is subscribed to an event with the decorator @EventHandler(<name of the event class>. Any time that a new event is added to the event store, the handle method in the event handler will be called with the instance of the event and the register object that can be used to emit new events. Event handlers can run arbitrary code and is where it is recommended to write most of the business logic in a reactive way.

Entities

To create an entity... You guessed it! We use the boost tool:

boost new:entity <name of the entity> --fields fieldName:fieldType --reduces EventOne EventTwo EventThree

For instance, running the following command:

boost new:entity Order --fields shippingAddress:Address orderItems:"Array<OrderItem>" --reduces OrderCreated

It will generate a class in the src/entities folder with the following structure:

@Entity
export class Order {
  public constructor(readonly id: UUID, readonly shippingAddress: Address, readonly orderItems: Array<OrderItem>) {}

  @Reduces(OrderCreated)
  public static createOrder(event: OrderCreated, previousOrder?: Order): Order {
    return event.order
  }
}

Entities are not shown in the diagram because they're just a different view of the data in the events store.

Entities represent domain model objects, that is, something that can be mapped to an object with semantics in your domain. Entities only exist conceptually, they're not explicitly stored in any database, but generated on the fly from a list of related events.

Booster creates snapshots of the entities automatically under the hoods to reduce access times, but the developer doesn't has to worry about that.

Examples of entities are:

As you can see, entities are also regular TypeScript classes, like the rest of the Booster artifacts.

Take a look, entities have a special reducer function decorated with @Reduces, that will be triggered each time that a specific kind of event is generated.

All projection functions receive:

And it always must return a new entity. This function must be pure, which means that it cannot perform any side effects, only create a new object based on some conditions on the input data, and then return it.

Reading Entity "state"

@Command({
  authorize: 'all',
})
export class MoveStock {
  public constructor(readonly productSKU: UUID, readonly fromLocationId: UUID, readonly toLocationId: UUID, readonly quantity: number) {}

  public handle(register: Register): void {
    const productStock = fetchEntitySnapshot('ProductStock', this.productSKU)

    if (productStock.locations[this.fromLocationId].count >= this.quantity) {
      // Enough stock, we confirm the movement
      register.events(new StockMovement(this.productSKU, this.fromLocationId, this.toLocationID, quantity))
    } else {
      // Not enough stock, we register this fact
      register.events(new FailedCommand({
        command: this,
        reason: `Not enough stock in origin location`
      ))
    }
  }
}

Booster provides a handy fetchEntitySnapshot method to check the value of an entity from any handler method in order to make domain-driven decisions.

Read Models - The Read Pipeline

TODO: (Not in the current release) To generate a Read Model you can use a read model generator. It works similarly than the entities generator:

boost new:read-model <name of the read model class> --fields fieldName:fieldType --projects EntityOne EntityTwo

Using the generator will generate a class with the following structure in src/read-models/<name-of-the-read-model>.ts. For instance:

boost new:read-model CartReadModel --fields id:UUID cartItems:"Array<CartItem>" paid:boolean --projects Cart

It will generate a class with the following structure:

@ReadModel
export class CartReadModel {
  public constructor(
    readonly id: UUID,
    readonly cartItems: Array<CartItem>,
    public paid: boolean
  ) {}

  @Projection(Cart, 'id')
  public static updateWithCart(cart: Cart, oldCartReadModel?: CartReadModel): CartReadModel {
    return new CartReadModel(cart.id, cart.cartItems, cart.paid)
  }
}

Read Models are cached data optimized for read operations and they're updated reactively when Entities are updated by new events. They also define the Read API, the available REST endpoints and their structure.

Read Models are classes decorated with the @ReadModel decorator that have one or more projection methods. A Projection is a method decorated with the @Projection decorator that, given a new entity value and (optionally) a previous read model state, generate a new read model value.

Read models can be projected from multiple entities as soon as they share some common key called joinKey.

Read Model classes can also be created by hand and there are no restrictions regarding the place you put the files. The structure of the data is totally open and can be as complex as you can manage in your projection functions.

Defining a read models enables a new REST Read endpoint that you can use to query or poll the read model records see the API documentation.

Authentication and Authorization

For example, the following command can be executed by anyone:

@Command({
  authorize: 'all',
})
export class CreateComment {
  ...
}

While this one can be executed by authenticated users that have the role Admin or User:

@Command({
  authorize: [Admin, User],
})
export class UpdateUser {
  ...
}

Authorization in Booster is done through roles. Every Command (and in the future, every ReadModel) has an authorize policy that tells Booster who can execute or access it. The policy is specified in the @Command decorator and consists of one of the following two values:

This is an example of a definition of two roles:

@Role({
  allowSelfSignUp: false,
})
export class Admin {}

@Role({
  allowSelfSignUp: true,
})
export class User {}

By default, a Booster application has no roles defined, so the only allowed value you can use in the authorize policy is 'all' (good for public APIs). If you want to add user authorization, you first need to create the roles that are suitable for your application. Roles are classes annotated with the @Role decorator, where you can specify some attributes.

Here, we have defined the Admin and User roles. The former contains the following attribute allowSelfSignUp: false, which means that when users sign-up, they can't specify the role Admin as one of its roles. The latter has this attribute set to true, which means that any user can self-assign the role User when signing up.

If your Booster application has roles defined, an authentication API will be provisioned. It will allow your users to gain access to your resources.

This API consists of three endpoints (see the API documentation):

Confirmation email Email confirmed

Once a user has an access token, it can be included in any request made to your Booster application as a Bearer Authorization header (Authorization: Bearer). It will be used to get the user information and authorize it to access protected resources.

Deploying

One of the goals of Booster is to become provider agnostic so you can deploy your application to any serverless provider like AWS, Google Cloud, Azure, etc...

So far, in the current version, only AWS is supported, but given the high level of abstraction, it will eventually support all cloud providers. (Contributions are welcome! 😜)

Configure your provider credentials

Creating a plain text file manually named ~/.aws/credentials with the following content will be enough:

[default]
aws_access_key_id = <YOUR KEY ID>
aws_secret_access_key = <YOUR ACCESS KEY>
region = eu-west-1

In the case of AWS, it is required that your ~/.aws/credentials are properly setup, and a region attribute is specified. If you have the AWS CLI installed, you can create the config file by running the command aws configure, but that is completely optional, AWS CLI is not required to run booster.

Deploy your project

To deploy your Booster project, run the following command:

boost deploy

It will take a while, but you should have your project deployed to your cloud provider.

If you make changes to your code, you can run boost deploy again to update your project in the cloud.

Deleting your cloud stack

If you want to delete the Booster application that has been deployed to the cloud, you can run:

boost nuke

Booster Cloud Framework REST API

The API for a Booster application is very simple and is fully defined by auth endpoints and the commands and read models names and structures.

After a successful deployment you'll see an "Outputs:" section in your terminal with several values that you need to use when doing requests to the API. Those values are:

Note that the Content-Type for all requests is application/json.

Authentication and Authorization API

The following endpoints are provisioned if your application have at least one role defined. For more information about how to use roles to restrict the access to your application, see the section Authentication and Authorization.

Sign-up

Register a user in your application. After a successful invocation, an email will be sent to the user's inbox with a confirmation link. Users's won't be able to sign-in before they click in that link.

Endpoint

http request POST https://<baseURL>/auth/sign-up

Request body

Sign-up response body

{
    "clientId": "string",
    "username": "string",
    "password": "string",
    "userAttributes": {
        "roles": ["string"]
    }
}
Parameter Description
clientId The application client Id that you got as an output when the application was deployed.
username The username of the user you want to register. It must be an email.
password The password the user will use to later login into your application and get access tokens.
userAttributes Here you can specify the attributes of your user. These are:
Response

An empty body

Errors

Sign-up error response body example: Not specifiying an email as username.

{
    "__type": "InvalidParameterException",
    "message": "Username should be an email."
}

You will get a HTTP status code different from 2XX and a body with a message telling you the reason of the error.

Sign-in

Allows your users to get tokens to be able to make request to restricted endpoints. Remember that before a user can be signed in into your application, its email must be confirmed

Endpoint

http request POST https://<baseURL>/auth/sign-in

Request body

Sign-in request body

{
    "clientId":"string",
    "username":"string",
    "password":"string"
}
Parameter Description
clientId The application client Id that you got as an output when the application was deployed.
username The username of the user you want to sign in. It must be previously signed up
password The password used to sign up the user.
Response

Sign-in response body

{
    "accessToken": "string",
    "expiresIn": "string",
    "refreshToken": "string",
    "tokenType": "string"
}
Parameter Description
accessToken The token you can use to access restricted resources. It must be sent in the Authorization header (prefixed with the tokenType)
expiresIn The period of time, in seconds, after which the token will expire
refreshToken The token you can use to get a new access token after it has expired.
tokenType The type of token used. It is always Bearer
Errors

Sign-in error response body example: Login of an user that has not been confirmed

{
    "__type": "UserNotConfirmedException",
    "message": "User is not confirmed."
}

You will get a HTTP status code different from 2XX and a body with a message telling you the reason of the error.

Sign-out

Finalizes the user session by cancelling their tokens.

Endpoint

http request POST https://<baseURL>/auth/sign-out

Request body

Sign-out request body json { "accessToken":"string" }

Parameter Description
accessToken The access token you get in the sign-in call.
Response

An empty body

Errors

Sign-out error response body example: Invalid access token specified

{
    "__type": "NotAuthorizedException",
    "message": "Invalid Access Token"
}

You will get a HTTP status code different from 2XX and a body with a message telling you the reason of the error.

Write API (commands submission)

POST https://<baseURL>/commands

Request body:

{
  "typeName": "ChangeCartItem",
  "version": 1,
  "value": {
    "cartId": "demo",
    "sku": "ABC-10",
    "quantity": 1
  }
}

Read API (retrieve a read model)

Get a list

GET https://<baseURL>/readmodels/<read model class name>

Example:

GET https://<baseURL>/readmodels/CartReadModel

Get a specific read model

GET https://<baseURL>/readmodels/<read model class name>/<read model ID>

Example:

GET https://<baseURL>/readmodels/CartReadModel/42

Frequently asked questions

1.- When deploying my application in AWS for the first time, I got an error saying "StagingBucket -toolkit-bucket already exists"

When you deploy a Booster application to AWS, an S3 bucket needs to be created to upload the application code. Booster names that bucket using your application name as a prefix. In AWS, bucket names must be unique globally, so if there is another bucket in the world with exactly the same name as the one generated for your application, you will get this error.

The solution is to change your application name in the configuration file so that the bucket name is unique.