Quickstart: Customer Registry in JavaScript

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

Before you begin

If you want to bypass writing code and jump straight to the deployment, take a look at our prebuilt samples.

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 Kalix kalix/annotations.proto.

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

    message Customer {
      string customer_id = 1 [(kalix.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 {
      option (kalix.service).acl.allow = { principal: ALL };
    
      rpc ChangeAddress(ChangeAddressRequest) returns (google.protobuf.Empty) {}
  5. Add the message that will identify which customer to retrieve for the GetCustomer message:

    message GetCustomerRequest {
      string customer_id = 1 [(kalix.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 Kalix 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;
    }

Add the Kalix JavaScript SDK

  1. Add the Kalix JavaScript SDK to your project.

    npm install @kalix-io/kalix-javascript-sdk --save

This adds the kalix-javascript-sdk package to your package.json as a dependency.

  "dependencies": {
    "@kalix-io/kalix-javascript-sdk": "1.0.0-M10"
  }

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 import statements for the JavaScript SDK to customer-value-entity.js

    const ValueEntity = require("@kalix-io/kalix-javascript-sdk").ValueEntity;
    const { replies } = require("@kalix-io/kalix-javascript-sdk");
  3. Create the ValueEntity object

    const entity = new ValueEntity(
      ["customer_api.proto", "customer_domain.proto"],
      "customer.api.CustomerService",
      "customers"
    );
  4. 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")
    }
  5. 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 }));
  6. Create the command handlers who, as the name suggests, handle incoming requests before persisting them

    entity.setCommandHandlers({
      Create: create,
      GetCustomer: getCustomer
    })
  7. 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.emptyReply()
    }
  8. Add the getCustomer method that handles requests to see customer data

    function getCustomer(request, state, ctx) {
      let apiCustomer = customerStateToApiCustomer(state)
      return replies.message(apiCustomer)
    }
  9. 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)
    }
  10. 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)
    }
  11. 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 Kalix = require("@kalix-io/kalix-javascript-sdk").Kalix
  3. Register and start the components

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

Package and deploy your service

To build the container image, publish it to your container registry and deploy from your registry, follow these steps:

  1. If you haven’t done so yet, sign in to your Kalix account. If this is your first time using Kalix, this will let you register an account, create your first project, and set this project as the default.

    kalix auth login
  2. 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"]
  3. Create a .dockerignore file in the root directory of your project. Add a single line to this file to ignore node_modules:

    node_modules
  4. 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. Run the kalix services deploy command to deploy your service using the container image from the container registry.

    kalix services deploy <name> <your container registry>/<your registry username>/<your projectname>
  3. You can verify the status of the deployed service using:

    kalix service list

Invoke your service

Once the service has started successfully, you can start a proxy locally to access the service:

kalix service proxy <service name> --grpcui

The --grpcui option also starts and opens a gRPC web UI for exploring and invoking the service (available at http://127.0.0.1:8080/ui/).

Or you can use command line gRPC or HTTP clients, such as grpcurl or curl, to invoke the service through the proxy at localhost:8080, using plaintext connections.

A customer can be created using the Create method on CustomerService, in the gRPC web UI, or with grpcurl:

grpcurl \
  -d '{
    "customer_id": "abc123",
    "email": "someone@example.com",
    "name": "Someone",
    "address": {
      "street": "123 Some Street",
      "city": "Somewhere"
    }
  }' \
  --plaintext localhost:8080 \
  customer.api.CustomerService/Create

The GetCustomer method can be used to retrieve this customer, in the gRPC web UI, or with grpcurl:

grpcurl \
  -d '{"customer_id": "abc123"}' \
  --plaintext localhost:8080 \
  customer.api.CustomerService/GetCustomer

You can expose the service to the internet. A generated hostname will be returned from the expose command:

kalix service expose <service name>

Next steps