Kickstart a Node module

The Kalix code generation tool for JavaScript helps you to get started quickly with a project that you can build using npm or Yarn. The tool generates the recommended project structure (including package.json), a Value Entity, unit tests, and a README.md that explains what was created and how to work with the newly created service.

You can modify the .proto and source files, and the tools will generate code stubs for the elements you changed or added. The generated project also contains scripts for running the service locally and for packaging into a container and deploying on Kalix

The term "project" used in connection with code generation is a development project that contains your service implementation. This differs from a Kalix project, which is where you deploy one or more packaged services.

Prerequisites

Before running the code generation tools, make sure that you have the following:

1. Prepare the development project

The code generation tools create a new development project named my-entity, that contains build configuration and the gRPC Protobuf specifications for a Value Entity.

  1. To create the initial codebase, from a command window enter the following:

    npm
    npx @kalix-io/create-kalix-entity@latest my-entity

    To generate TypeScript sources add the --typescript option:

    npx @kalix-io/create-kalix-entity@latest my-entity --typescript
    yarn
    yarn create @kalix-io/kalix-entity@latest my-entity

    To generate TypeScript sources add the --typescript option:

    yarn create @kalix-io/kalix-entity@latest my-entity --typescript
  2. Change directories to the newly created folder:

    cd my-entity
  3. Prepare the build unit by installing all the required dependencies and tools:

    npm
    npm install
    yarn
    yarn

Take a look at the initial structure. The project contains the following directories:

  • lib - typescript files that allow IDEs to provide better highlighting and validation.

  • proto - files containing domain and api Protobuf definitions for a Counter Value Entity.

To understand how the Protobuf definitions relate to the implementation files, see The development process and Understanding the programming model.

2. Build and generate code

During the build process, the tools generates source files and unit test templates for a Value Entity. You can change or add to the .proto and source files. The next build will generate classes and tests for your additions if they do not exist, but will not overwrite existing classes or tests.

Build the project to generate source code:

npm
npm run build
yarn
yarn build

You should now see the following source files in your project:

  • src/counter.js - the implementation of the counter Value Entity.

  • src/index.js - a convenience file that starts the Kalix proxy for running locally

  • test/counter.test.js - a test for the counter Value Entity.

The lib/generated directory contains files used by the tools. Any changes made to files in this folder are overwritten on the next build.

2.1. Unit and integration tests

Kalix codegen creates a unit test stub for each class within counter.test.js. Use these stubs as a starting point to test the logic in your implementation. You can use any of the common Node.js testing tools, such as Mocha or Jest.

JavaScript
/*
 * Copyright 2021-2023 Lightbend Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { MockValueEntity } from "@kalix-io/testkit";
import { expect } from "chai";
import counter from "../src/counter.js";

const CounterState = counter.lookupType("com.example.domain.CounterState");

describe("CounterService", () => {
  const entityId = "entityId";

  describe("Increase", () => {

    it("should increase the value with no prior state", async () => {
      const entity = new MockValueEntity(counter, entityId);
      const result = await entity.handleCommand("Increase", { entityId: entityId, value: 42 });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 42 }));
    });

    it("should increase the value with some prior state", async () => {
      const entity = new MockValueEntity(counter, entityId);
      entity.state = CounterState.create({ value: 13 });
      const result = await entity.handleCommand("Increase", { entityId: entityId, value: 42 });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 13 + 42 }));
    });

    it("should fail on negative values", async () => {
      const entity = new MockValueEntity(counter, entityId);
      const result = await entity.handleCommand("Increase", { entityId: entityId, value: -2 });

      expect(result).to.be.undefined;
      expect(entity.error).to.be.equal(`Increase requires a positive value. It was [-2].`);
    });
  });

  describe("Decrease", () => {
    it("should decrease the value with no prior state.", async () => {
      const entity = new MockValueEntity(counter, entityId);
      const result = await entity.handleCommand("Decrease", { entityId: entityId, value: 42 });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: -42 }));
    });
  });

  describe("Reset", () => {
    it("should reset the entity value to 0", async () => {
      const entity = new MockValueEntity(counter, entityId);
      entity.state = CounterState.create({ value: 13 });
      const result = await entity.handleCommand("Reset", { entityId: entityId });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 0 }));
    });
  });

  describe("GetCurrentCounter", () => {
    it("should return the current state", async () => {
      const entity = new MockValueEntity(counter, entityId);
      entity.state = CounterState.create({ value: 13 });
      const result = await entity.handleCommand("GetCurrentCounter", { entityId: entityId });

      expect(result).to.deep.equal({ value: 13 });
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 13 }));
    });
  });
});
TypeScript
/*
 * Copyright 2021-2023 Lightbend Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { MockValueEntity } from "@kalix-io/testkit";
import { expect } from "chai";
import counterEntity from "../src/counter";

const CounterState = counterEntity.lookupType("com.example.domain.CounterState");

describe("CounterService", () => {
  const entityId = "entityId";

  describe("Increase", () => {
    it("should increase the value with no prior state", async () => {
      const entity = new MockValueEntity(counterEntity, entityId);
      const result = await entity.handleCommand("Increase", {
        entityId: entityId,
        value: 42
      });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 42 }));
    });

    it("should increase the value with some prior state", async () => {
      const entity = new MockValueEntity(counterEntity, entityId);
      entity.state = CounterState.create({ value: 13 });
      const result = await entity.handleCommand("Increase", {
        entityId: entityId,
        value: 42
      });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 13 + 42 }));
    });

    it("should fail on negative values", async () => {
      const entity = new MockValueEntity(counterEntity, entityId);
      const result = await entity.handleCommand("Increase", {
        entityId: entityId,
        value: -2
      });

      expect(result).to.be.undefined;
      expect(entity.error).to.be.equal(`Increase requires a positive value. It was [-2].`);
    });
  });

  describe("Decrease", () => {
    it("should decrease the value with no prior state.", async () => {
      const entity = new MockValueEntity(counterEntity, entityId);
      const result = await entity.handleCommand("Decrease", {
        entityId: entityId,
        value: 42
      });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: -42 }));
    });
  });

  describe("Reset", () => {
    it("should reset the entity value to 0", async () => {
      const entity = new MockValueEntity(counterEntity, entityId);
      entity.state = CounterState.create({ value: 13 });
      const result = await entity.handleCommand("Reset", { entityId: entityId });

      expect(result).to.deep.equal({});
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 0 }));
    });
  });

  describe("GetCurrentCounter", () => {
    it("should return the current state", async () => {
      const entity = new MockValueEntity(counterEntity, entityId);
      entity.state = CounterState.create({ value: 13 });
      const result = await entity.handleCommand("GetCurrentCounter", {
        entityId: entityId
      });

      expect(result).to.deep.equal({ value: 13 });
      expect(entity.error).to.be.undefined;
      expect(entity.state).to.deep.equal(CounterState.create({ value: 13 }));
    });
  });
});

The mock entity used by counter.test.js is provided by the @kalix-io/testkit package.

3. Run locally

You can run your services locally and test them manually with HTTP or gRPC requests. To run locally, you need to initiate the Kalix proxy. The docker-compose file contains the configuration required to run the proxy.

  1. To start the proxy, run the following command from the top-level project directory:

docker compose up
  1. Start the application locally with the following command:

    npm
    npm run start
    yarn
    yarn start
For more details see Run a service locally.

4. Package service

The project contains configuration files for packaging the service into a Docker image, which can then be deployed to Kalix. The Docker image name can be changed in the package.json config.dockerImage string value. Update this configuration to publish your image to your Docker repository.

The recommended version is Node 18 and the image is based on the official Node Docker Image. Choose a different image in the Dockerfile placed in the root folder of the development project.

npm
npm run package
yarn
yarn package

5. Deploy to Kalix

The build tool script conveniently packages and publishes the Docker image prior to deployment.

To deploy your service:

  1. Update the dockerImage property in the project’s package.json file.

    Refer to Configuring registries for more information on how to make your docker image available to Kalix.

  2. In your command window, set your Kalix project to be the current project:

    kalix config set project <project-name>
  3. Run the deployment scripts:

    npm
    npm run deploy
    yarn
    yarn deploy

Summary

The code generation tools created a project and supporting resources to kickstart your development. Modify the .proto and implementations or replace them with your own and take advantage of the generated resources. To learn more about the generated Counter Service, see Implementing Value Entities in JavaScript.