Quickstart: Customer Registry in JavaScript

Learn how to create a customer registry in JavaScript, package it into a container, and run it on Akka Serverless.

Before you begin

Writing the Customer Registry

  1. From the command line, create a directory for your project.

    mkdir customerregistry
  2. Change into the project directory.

    cd customerregistry
  3. Run the npm init command, accepting default values

    npm init -y
  4. Add these additional scripts to the scripts property in your package.json

"scripts": {
    "build": "compile-descriptor customer_api.proto customer_domain.proto",
    "pretest": "npm run build",
    "test": "mocha",
    "start": "node index.js",
}

Define the external API

The Customer Registry service will create or retrieve a customer, including their email, phone number and mailing address. The customer_api.proto will contain the external API your clients will invoke.

  1. Create a customer_api.proto file and save it in the root directory of your project.

  2. Add declarations for:

    • The protobuf syntax version, proto3.

    • The package name, customer.api.

    • Import google/protobuf/empty.proto and Akka Serverless akkaserverless/annotations.proto.

      syntax = "proto3";
      
      package customer.api;
      
      import "google/protobuf/empty.proto";
      import "akkaserverless/annotations.proto";
  3. Add the service endpoint

    message Customer {
      string customer_id = 1 [(akkaserverless.field).entity_key = true];
      string email = 2;
      string name = 3;
      Address address = 4;
    }
    
    message Address {
      string street = 1;
      string city = 2;
    }
  4. Add messages to define the fields that comprise a Customer object (and its compound Address)

    service CustomerService {
      rpc Create(Customer) returns (google.protobuf.Empty) {}
    
      rpc GetCustomer(GetCustomerRequest) returns (Customer) {}
    }
  5. Add the message that will identify which customer to retrieve for the GetCustomer message:

    message GetCustomerRequest {
      string customer_id = 1 [(akkaserverless.field).entity_key = true];
    }
  6. Save and close the customer_api.proto file.

Define the domain model

The customer_domain.proto file contains all the internal data objects (Entities). The Value Entity in this quickstart is a Key/Value store that stores only the latest updates.

  1. Create a customer_domain.proto file and save it in the root directory of your project.

  2. Add declarations for the proto syntax, the Akka Serverless annotations, and package name

    syntax = "proto3";
    
    package customer.domain;
  3. Add the CustomerState message with fields for entity data, and the Address message that defines the compound address:

    message CustomerState {
      string customer_id = 1;
      string email = 2;
      string name = 3;
      Address address = 4;
    }
    
    message Address {
      string street = 1;
      string city = 2;
    }

Implement your business logic

  1. Create a customer-value-entity.js file and save it in the root directory of your project.

  2. Add the Akka Serverless JavaScript SDK to your package.json file

    npm install @lightbend/akkaserverless-javascript-sdk --save
  3. Add the import statements for the JavaScript SDK to customer-value-entity.js

    const ValueEntity = require("@lightbend/akkaserverless-javascript-sdk").ValueEntity;
    const { replies } = require("@lightbend/akkaserverless-javascript-sdk");
  4. Create the ValueEntity object

    const entity = new ValueEntity(
      ["customer_api.proto", "customer_domain.proto"],
      "customer.api.CustomerService",
      "customers"
    );
  5. Create objects for the internal and external representations of your customer

    const domainPkg = "customer.domain.";
    const domain = {
      CustomerState: entity.lookupType(domainPkg + "CustomerState"),
      Address: entity.lookupType(domainPkg + "Address"),
    }
    const apiPkg = "customer.api."
    const api = {
      Customer: entity.lookupType(apiPkg + "Customer")
    }
  6. Create the "initial state" for the entities (this method is called when no other data can be found for your entity)

    entity.setInitial(customerId => domain.CustomerState.create({ customerId: customerId }));
  7. Create the command handlers who, as the name suggests, handle incoming requests before persisting them

    entity.setCommandHandlers({
      Create: create,
      GetCustomer: getCustomer
    })
  8. Add the create method that handles incoming requests to create new customers

    function create(customer, customerState, ctx) {
      let domainCustomer = apiCustomerToCustomerState(customer)
      ctx.updateState(domainCustomer)
      return replies.noReply()
    }
  9. Add the getCustomer method that handles requests to see customer data

    function getCustomer(request, state, ctx) {
      let apiCustomer = customerStateToApiCustomer(state)
      return replies.message(apiCustomer)
    }
  10. At the bottom of the file, add a method customerStateToApiCustomer to convert the state of the customer to a response message for your external API

    function customerStateToApiCustomer(customerState) {
      // right now these two have the same fields so conversion is easy
      return api.Customer.create(customerState)
    }
  11. Add a apiCustomerToCustomerState method to convert the incoming request to your domain model

    function apiCustomerToCustomerState(apiCustomer) {
      // right now these two have the same fields so conversion is easy
      return domain.CustomerState.create(apiCustomer)
    }
  12. Add an export statement so the index.js file can access your module

    module.exports = entity;

Create the index.js file

  1. Create an index.js file and save it in the root directory of your project.

  2. Add the import statement for the JavaScript SDK to index.js

    const AkkaServerless = require("@lightbend/akkaserverless-javascript-sdk").AkkaServerless
  3. Register and start the components

    console.log("Starting Value Entity")
    const server = new AkkaServerless();
    server.addComponent(require("./customer-value-entity-view"))
    server.start()
    // end::register[]

Package and deploy your service

To compile, build the container image, and publish it to your container registry, follow these steps

  1. Create a dockerfile in the root directory of your project

    # This Dockerfile uses multi-stage build process.
    # See https://docs.docker.com/develop/develop-images/multistage-build/
    
    # Stage 1: Downloading dependencies and building the application
    FROM node:14.17.0-buster-slim AS builder
    
    # Set the working directory
    WORKDIR /home/node
    
    # Install app dependencies
    COPY package*.json ./
    RUN npm ci
    
    # Copy sources and build the app
    COPY . .
    RUN npm run build
    
    # Remove dev packages
    # (the rest will be copied to the production image at stage 2)
    RUN npm prune --production
    
    # Stage 2: Building the production image
    FROM node:14.17.0-buster-slim
    
    # Set the working directory
    WORKDIR /home/node
    
    # Copy dependencies
    COPY --from=builder --chown=node /home/node/node_modules node_modules/
    
    # Copy the app
    COPY --from=builder --chown=node \
        /home/node/package*.json \
        /home/node/*.js \
        /home/node/*.proto \
        /home/node/user-function.desc \
        ./
    
    # Run the app as an unprivileged user for extra security.
    USER node
    
    # Run
    EXPOSE 8080
    CMD ["npm", "start"]
  2. Run the docker build command to build your container image

    docker build . -t <your container registry>/<your registry username>/<your projectname>

When you’re using Docker Hub, you only need to specify <your registry username>/<your projectname> (like myuser/myproject)

  1. Run the docker push command to push the container image to a container registry

    docker push <your container registry>/<your registry username>/<your projectname>
  2. Sign in to your Akka Serverless account at: https://console.akkaserverless.com/

  3. If you do not have a project, click Add Project to create one, otherwise choose the project you want to deploy your service to.

  4. On the project dashboard click the "+" next to services to start the deployment wizard

  5. Choose a name for your service and click Next

  6. Enter the container image URL from the above step and click Next

  7. Click Next (no environment variables are needed for these samples)

  8. Check both Add a route to this service and Enable CORS and click Next

  9. Click Finish to start the deployment

  10. Click Go to Service to see your newly deployed service

Invoke your service

Now that you have deployed your service, the next step is to invoke it using gRPCurl

  1. From the "Service Explorer" click on the method you want to invoke

  2. Click on "gRPCurl"

  3. In the bottom section of the dialog, fill in the values you want to send to your service

  4. In the top section of the dialog, click the "Copy to clipboard" button

  5. Open a new command line and paste the content you just copied