Quickstart: Shopping Cart in JavaScript
Learn how to create a shopping cart in JavaScript, package it into a container, and run it on Kalix.
Before you begin
-
If you’re new to Kalix, create an account
so you can try it out for free.
-
You’ll also need to install the Kalix CLI to deploy from a terminal window.
-
For this quickstart, you’ll also need
If you want to bypass writing code and jump straight to the deployment:
|
Create the project structure and install dependencies
-
From the command line, create a directory with the basic structure for your project using a template:
npx @kalix-io/create-kalix-entity@latest shopping-cart --template basic
-
Change into the project directory:
cd shopping-cart
-
Download and install project dependencies:
npm install
Define the external API
The Shopping Cart service will store shopping carts for your customers, including the items in those carts. The shoppingcart_api.proto
will contain the external API your clients will invoke.
-
Create a
proto
directory.mkdir proto
-
Create a
shopping_cart_api.proto
file and save it in theproto
directory. -
Add declarations for:
-
The protobuf syntax version,
proto3
. -
The package name,
shopping.cart.api
. -
Import
google/api/annotations.proto
,google/protobuf/empty.proto
, and Kalixkalix/annotations.proto
.
proto/shopping_cart_api.protosyntax = "proto3"; package shopping.cart.api; import "google/api/annotations.proto"; import "google/protobuf/empty.proto"; import "kalix/annotations.proto";
-
-
Add the service endpoint. The service endpoint is annotated with
kalix.codegen
indicating we want to generate an Event Sourced Entity for this service.proto/shopping_cart_api.protoservice ShoppingCart { option (kalix.codegen) = { event_sourced_entity: { name: "shopping.cart.domain.ShoppingCart" entity_type: "shopping-cart" state: "shopping.cart.domain.CartState" events: "shopping.cart.domain.ItemAdded" events: "shopping.cart.domain.ItemRemoved" } }; rpc AddItem(AddLineItem) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/cart/{cart_id}/items/add" body: "*" }; } rpc RemoveItem(RemoveLineItem) returns (google.protobuf.Empty) { option (google.api.http).post = "/cart/{cart_id}/items/{product_id}/remove"; } rpc GetCart(GetShoppingCart) returns (Cart) { option (google.api.http) = { get: "/carts/{cart_id}" additional_bindings: { get: "/carts/{cart_id}/items" response_body: "items" } }; } }
-
Add messages to define the fields that comprise a
Cart
object (and its compoundLineItem
):proto/shopping_cart_api.protomessage Cart { repeated LineItem items = 1; } message LineItem { string product_id = 1; string name = 2; int32 quantity = 3; }
-
Add the messages that are the requests to the shopping cart service:
proto/shopping_cart_api.protomessage AddLineItem { string cart_id = 1 [(kalix.field).entity_key = true]; string product_id = 2; string name = 3; int32 quantity = 4; } message RemoveLineItem { string cart_id = 1 [(kalix.field).entity_key = true]; string product_id = 2; } message GetShoppingCart { string cart_id = 1 [(kalix.field).entity_key = true]; }
Define the domain model
The shopping_cart_domain.proto
contains all the internal data objects (Entities). The Event Sourced Entity in this Quickstart keeps all events sent for a specific shopping cart in a journal.
-
Create a
shopping_cart_domain.proto
file and save it in theproto
directory. -
Add declarations for the proto syntax and domain package.
proto/shopping_cart_domain.protosyntax = "proto3"; package shopping.cart.domain;
-
Add the
CartState
message with fields for entity data and theLineItem
message that defines the compound line item:proto/shopping_cart_domain.protomessage CartState { repeated LineItem items = 1; } message LineItem { string productId = 1; string name = 2; int32 quantity = 3; }
-
Event Sourced entities work based on events. Add the events that can occur in this Quickstart:
message ItemAdded { LineItem item = 1; } message ItemRemoved { string productId = 1; }
Generate JavaScript implementation stubs
Run code generation to build JavaScript implementation stubs from your external API and domain model proto files.
npm run build
Implement shopping cart business logic
Fill in the implementation for the behavior of your shopping cart, which consists of Command Handlers and Event Handlers.
-
Command Handlers, as the name suggests, handle incoming API requests. State is not updated directly by command handlers. Instead, if state should be updated, an event is persisted that describes the intended transaction.
-
Event Handlers maintain the state of an entity by sequentially applying the effects of events to the local state.
-
Open
src/shoppingcart.js
. -
Fill in the implementation for the behavior of your shopping cart, which will be implemented by functions added below:
src/shoppingcart.jsentity.setBehavior(state => ({ commandHandlers: { AddItem: addItem, RemoveItem: removeItem, GetCart: getCart }, eventHandlers: { ItemAdded: itemAdded, ItemRemoved: itemRemoved } }));
-
Add the
addItem
function to handle requests to add items to a shopping cart.-
This function will handle an incoming
AddItem
request, and emit anItemAdded
event. -
The current state of the shopping cart is passed to the function but is not used.
-
It fails the command for an invalid quantity (needs to be at least one item).
-
Or it persists an
ItemAdded
event using the contextemit
effect.
src/shoppingcart.jsfunction addItem(addItem, _cart, ctx) { if (addItem.quantity < 1) { return Reply.failure( `Quantity for item ${addItem.productId} must be at least one.` ); } else { const itemAdded = ItemAdded.create({ item: { productId: addItem.productId, name: addItem.name, quantity: addItem.quantity } }); ctx.emit(itemAdded); return Reply.message({}); } }
-
-
Add the
removeItem
function to handle requests to remove items from a shopping cart.-
This function will handle an incoming
RemoveItem
request, and emit anItemRemoved
event. -
The current state of the shopping cart is passed and used to check the item exists.
-
It fails if the item to be removed is not found in the shopping cart.
-
If the item exists, it persists an
ItemRemoved
event using the contextemit
effect.
src/shoppingcart.jsfunction removeItem(removeItem, cart, ctx) { const existing = cart.items.find(item => item.productId === removeItem.productId ); if (!existing) { return Reply.failure( `Cannot remove item ${removeItem.productId} because it is not in the cart.` ); } else { const itemRemoved = ItemRemoved.create({ productId: removeItem.productId }); ctx.emit(itemRemoved); return Reply.message({}); } }
-
-
Add the
getCart
function to handle requests to retrieve a shopping cart.-
This function takes the current internal state and converts it to the external API model.
-
The conversion between the domain and the external API is straightforward, as they have the same fields.
src/shoppingcart.jsfunction getCart(_getShoppingCart, cart) { // API and domain messages have the same fields so conversion is easy return Reply.message(cart); }
-
-
Add the
itemAdded
function to update the state for emittedItemAdded
events.-
This function first checks for an existing line item for the newly added product.
-
If an existing item is found, its quantity is adjusted.
-
Otherwise, the new item is added directly to the cart.
-
Finally, the updated cart state is returned.
src/shoppingcart.jsfunction itemAdded(added, cart) { const existing = cart.items.find(item => item.productId === added.item.productId ); if (existing) { existing.quantity = existing.quantity + added.item.quantity; } else { cart.items.push(added.item); } return cart; }
-
-
Add the
itemRemoved
function to update the state for emittedItemRemoved
events.-
This function removes an item from the cart, by filtering it from the cart items.
-
The updated cart state is then returned.
src/shoppingcart.jsfunction itemRemoved(removed, cart) { cart.items = cart.items.filter(item => item.productId !== removed.productId ); return cart; }
-
The |
Package and deploy your service
To build and publish the container image and then deploy the service, follow these steps:
-
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
-
Update the
config.dockerImage
setting in thepackage.json
file with your container registry. -
Use the
deploy
script to build the container image, publish it to the container registry as configured in thepackage.json
file, and then automatically deploy the service to Kalix usingkalix
:npm run deploy
-
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 shopping-cart --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.
Items can be added to a shopping cart using the AddItem
method on the ShoppingCart
service, in the gRPC web UI, or with grpcurl
:
grpcurl \
-d '{
"cart_id": "abc123",
"product_id": "AAPL",
"name": "Apples",
"quantity": 42
}' \
--plaintext localhost:8080 \
shopping.cart.api.ShoppingCart/AddItem
The GetCart
method can be used to retrieve this cart, in the gRPC web UI, or with grpcurl
:
grpcurl \
-d '{"cart_id": "abc123"}' \
--plaintext localhost:8080 \
shopping.cart.api.ShoppingCart/GetCart
You can expose the service to the internet. A generated hostname will be returned from the expose command:
kalix service expose shopping-cart
Next steps
-
You can learn more about Event Sourced Entities.