Getting started with Typescript

The first steps to get started using our Typescript/Javascript client for building applications with Event Sourcing and CQRS.

In this article you will learn how to configure and create the different clients the Typescript/Javascript client library provides and get to know a bit about when to use which client. The code examples is for Typescript but it's possible us use the client for regular Javascript too (see more on github).

The client source code is available on Github. If you're interested in helping out with the client, please let us know!

Installing the client library

The client is available via NPM. To add the client to your project, install the client using the following command:

npm install @serialized/serialized-client

Configuring the client

To use the Javascript client you start by configuring it for your project. The API keys for your project can be found by logging in to the Web Console , navigating to your project and selecting API Keys.

// Create an instance of the Serialized client
import {Serialized} from "@serialized/serialized-client"

const serialized = Serialized.create({
    accessKey: "<YOUR_ACCESS_KEY>", 
    secretAccessKey: "<YOUR_SECRET_ACCESS_KEY>"
});

You find your API keys in the Serialized Console under Project settings:

The client library consists of a number of different clients that serves the need for the different kinds of application or service you are developing.

You use the Aggregates client to work with aggregates and their events. An aggregate is a concept from Domain-Driven Design that serves as a consistency boundary within your application. This is the entity in your system that emits your domain events.

Domain classes

Start with the domain. Create domain classes for the aggregate, state and events that your application should support. In this case we're implementing a simple Game aggregate to illustrate the client methods.

// The different statuses our game can be in
enum GameStatus {
  UNDEFINED = 'UNDEFINED',
  CREATED = 'CREATED',
  STARTED = 'STARTED',
  FINISHED = 'FINISHED'
}

type GameState = {
  readonly gameId?: string;
  readonly status?: GameStatus;
}

The events that our simple game supports:

// Example event classes
class GameCreated implements DomainEvent {
  constructor(readonly gameId: string,
              readonly creationTime: number) {
  };
}

class GameStarted implements DomainEvent {
  constructor(readonly gameId: string,
              readonly startTime: number) {
  };
}

Event handlers

Event handlers are functions that return a new state based on a previous state and an event. They must not have any side-effects and should have the signature:

// Example signature of event handler

@EventHandler(SomeEvent)
  handleSomeEvent(event: SomeEvent, state: State): State { 
    return {}; // new State
  }

Each aggregate needs to have its state represented by a materialization of the events into a current state. To create the current state we define a separate GameStateBuilder class with an initialState property and @EventHandler methods for each event type that ourGame aggregate needs for its state:

class GameStateBuilder {

  get initialState(): GameState {
    return {
      status: GameStatus.UNDEFINED
    }
  }

  @EventHandler(GameCreated)
  handleGameCreated(event: GameCreated, state: GameState): GameState {
    return {gameId: state.gameId, status: GameStatus.CREATED};
  }

  @EventHandler(GameStarted)
  handleGameStarted(event: GameStarted, state: GameState): GameState { 
    return {...state, status: GameStatus.STARTED};
  }

}

Define the @Aggregate class

The aggregate class contains our business logic methods. The business methods operate on the current state and should not have any side effects. Each method return a list of new domain events on success and if the command fails the method should throw an error.

Below is a simple example that supports creating and starting games:

@Aggregate('game', GameStateBuilder)
class Game {

  constructor(private readonly state: GameState) {
  }

  create(gameId: string, creationTime: number) {
    return [new GameCreated(gameId, creationTime)];
  }

  start(gameId: string, startTime: number) {
    if(this.state.status !== GameStatus.CREATED) {
      throw new Error('Must create Game before you can start it');
    }
    return [new GameStarted(gameId, startTime)];
  }

}

Saving an aggregate

Now we are ready to start using our domain classes and save events to Serialized using the client. You access the aggregate client via aggregateClient(AggregateClass) method.

The aggregate saves your domain events to Serialized maintains the consistency of your aggregates via optimistic concurrency control. This client serves as a core infrastructure component in the write-side of your CQRS-application.

To save a new aggregate you call the create method on the client:

// Creates a new Game with a random id
import {v4 as uuidv4} from 'uuid';
const gameClient = serialized.aggregateClient(Game);

const createGame = async function(gameId = uuidv4()) {
    await gameClient.create(gameId, (game) => ({
          events: game.create(gameId, Date.now())
        }));
}

Note: The create method verifies that there are no previously stored aggregates with the provided aggregate id.

How to update an aggregate

Updating an aggregate involved three different steps that must happen atomically:

  • Load all previously stored events
  • Invoke a command on the @Aggregate class (domain logic).
  • Store new events that were emitted from the aggregate class

Using the update method on the client handles these three operations for you.

When the current state has been created, the client creates an aggregate instance by calling the constructor of the @Aggregate-decorated class with the loaded state.

During the update, a command method on the aggregate instance can be called which will either succeed and return a list of events or fail and throw an error. If the command succeeds, the client will store the events by appending them in the event store for the aggregate. There is a check when update is called to make sure that there has not been any changes to this aggregate between the loading of events and the storing of new events, making the update-operation consistent and safe.

To update an aggregate using the OCC control mechanism in Serialized you use the update method on the AggregateClient:

// Starts the game with the given id 
const startGame = async function(gameId) {
  await gameClient.update(gameId, (game: Game) =>
        ({events: game.start(gameId, startTime)}));
}

Projections client

To serve data to your client applications you will soon want to transform your events to tailored projections (or read-models). The projections client helps you define these projection transformation definitions and to query the projection data that is stored as a result of these transformations.

The entry point for the Projection client is serialized.projections and to create a projection (in this case a User) you can execute the following code:

var projectionsClient = serialized.projections;
var userProjectionDefinition = {
 feedName: 'user-registration',
 projectionName: 'user',
 handlers: [
   {
     eventType: 'UserRegistrationCompleted',
     functions: [
       {
         function: 'merge',
       }
     ],
   }
 ]
};
await projectionsClient.createOrUpdateDefinition(userProjectionDefinition)

To read more about the different projection functions available and how to use them, see the documentation about event handlers.

Reactions client

To efficiently build event-driven workflows and integration points you can use the reactions client to define event-triggered reactions. You can also use the Reactions client to provide scheduling functionality to your application.

The Reactions client has its entry point in serialized.reactions. To create a Reaction definition that will React on any new UserRegistrationCompleted events we can write the following code:

var reactionsClient = serialized.reactions;
var sendEmailAction: HttpAction = {
 actionType: 'HTTP_POST',
 targetUri: 'https://some-email-service'
};
var reactionDefinition = {
 reactionName: 'email-registered-user',
 feedName: 'user-registration',
 reactOnEventType: 'UserRegistrationCompleted',
 action: sendEmailAction
}

await reactionsClient.createOrUpdateReactionDefinition(reactionDefinition);

Feed client

The feed client provides a raw, Atom-like feed of the events you store. It provides different filters and query options that you use to build custom read-models or event-triggers when the Projections/Reactions clients are not dynamic or flexible enough for your use case.

The entry point for the Feed client is the class serialized.feeds. To load a feed of all UserRegistered events we can write the following code:

var feedsClient = serialized.feeds;
var options = {
  since: 0,
  limit: 10
}
var request = {
  feedName: 'user-registration'
}
await feedsClient.loadFeed(request, options);

Hope this initial guide has helped you get started with Serialized using Javascript!