<!-- added typescript here to the course title -->
# Build and Interact With a Decentralized Azle Canister Using Typescript
<!-- added typescript here to the course goal -->
In this tutorial, we will be building a Todo web3 canister in typescript. This canister will have basic CRUD(Create, Read, Update Delete) functionality.
<!-- added more azle info -->
We will be using the Azle framework to build our canister. Azle is a framework used for developing decentralized applications on ICP. It aids in writing, deploying and interacting with canisters - the basic unit of computation on ICP
<!-- added beta staus disclaimer -->
**Note: Azle is still in Beta mode and is not recommended for production use.**
Finally, we will be using the Candid web interface to interact with our canister.
## What you'll learn
- How to build a web3 canister
- How to deploy and interact with a web3 canister
## What is a Canister?
A canister is a fundamental building block and execution environment for deploying and running software applications.
Canisters are similar to smart contracts on other blockchain platforms, but they offer more powerful features and capabilities.
Canisters on the Internet Computer enable the development of scalable and decentralized applications.
They offer a secure and efficient execution environment, enabling developers to build a wide range of applications, including decentralized finance (DeFi) platforms, social media applications, decentralized autonomous organizations (DAOs), and more.
### Prerequisites
While having some prior coding experience is beneficial, it is not a hard requirement for this tutorial. However, we do recommend that you have the following:
- **Basic knowledge of JavaScript or Typescript** - Although this isn't strictly necessary, it could prove beneficial.
- **Knowledge of how to use your terminal** - This tutorial will require you to use your terminal. If you're unfamiliar with the terminal or need a refresher, watch this beginner tutorial on how to use the terminal: [Learn the Command Line: Basic Commands](https://www.youtube.com/watch?v=MBBWVgE0ewk).
- **Familiarity with IDEs** - This tutorial utilizes the web version of VSCode. If you're unfamiliar with IDEs or need a refresher, watch this beginner tutorial on how to use VSCode: [Learn Visual Studio Code in 7min (Official Beginner Tutorial)](https://www.youtube.com/watch?v=B-s71n0dHUk).
- **A GitHub account** - This tutorial utilizes GitHub Codespaces. If you don't have a GitHub account yet, create one at [github.com](https://github.com/)
### Tech Stack
<!-- added typescript -->
- [Azle](https://demergent-labs.github.io/azle/the_azle_book.html) - A framework for building web3 canisters in typescript.
- [TypeScript](https://nextjs.org/) - A popular open-source framework for building React applications with server-side rendering, automatic code splitting, and optimized performance.
- [Codespaces](https://github.com/features/codespaces) - A feature of GitHub that allows you to develop entirely in the cloud. It is a great tool to get started quickly without having to install anything on your computer.
### Overview
1. [Setup](#1-setup) (10 min) - This section will guide you through the necessary steps to set up your project.
2. [Building the canister](#2-building-the-canister) (20 min) - In this section, we will build a Todo canister with basic CRUD(Create, Read, Update Delete) functionality.
3. [Deploy and Interact With our Canister](#3-build-and-deploy-our-canister) (5 min) - In this section, we will interact with the Todo canister via our command line and the Candid web interface.
4. [Conclusion](#4-conclusion) (2 min) - Finally, we will conclude this tutorial and give you some ideas on how to continue.
If you want to skip to the complete code, you can find the GitHub repository [here](https://github.com/).
## 1. Setup
In this initial section, we are going to set up our boilerplate code and run it in a Codespace or in your local machine.
### 1.1 Setup our boilerplate
If you don't have a GitHub account yet, create one at [github.com](https://github.com/).
GitHub is a platform where developers can collaborate on projects and share code. It is also the platform where the boilerplate code is hosted and that we will use to create a Codespace.
Note: If you prefer to use your local machine to take this course instead of using the online codespace, you can click here and clone the boilerplate repository. https://github.com/dacadeorg/dfinity-boilerplate
Visit the [Github Codespace](https://github.com/codespaces) and click on the "Use this template" button on the Blank template.This action will spawn a new Codespace where you can build out this project without installing anything locally on your system.
## 2. Building the Canister
In this section, we will build a Todo canister with basic CRUD(Create, Read, Update Delete) functionality.
### 2.1 Setting up our terminal
We will first begin by installing nvm and node. Nvm is a version manager for node. It allows you to install multiple versions of node and switch between them. We will be using nvm to install node version 14.17.0. Node is a JavaScript runtime environment that allows you to run JavaScript outside of the browser. We will be using node to run our Azle project.
- Firstly let us install `nvm` with the following command:
```curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash```
- Next we will switch to node version 18 with the following command:
``` nvm use 18```
- Go ahead and install DFX which is a command-line interface for the Internet Computer. We will be using DFX to create our Azle project. Run the following command to install DFX:
```DFX_VERSION=0.15.0 sh -ci "$(curl -fsSL https://sdk.dfinity.org/install.sh)"```
- Now that we have DFX installed, we need to add it to our path. Run the following command to add DFX to your path:
```echo 'export PATH="$PATH:$HOME/bin"' >> "$HOME/.bashrc"```
- Finally, initialize a default Node.js project with the following command:
```npm init -y```
- If you are using the Github codespace, you will need to reload your terminal. You can do this by clicking on the "Reload" button in the top right corner of your terminal.
- **Note** If you are using Apple Silicon, you will need to install "esbuild-wasm" which is a dependency of Azle. Run the following command to install it:
```npm install esbuild-wasm```
### 2.2 Setting up our Azle project
Now that we have our terminal set up, we can begin creating our canister.
- Firsty, let us create a typescript config file with the following command:
<!-- remove unecessary file creation process -->
```touch tsconfig.json```
or create it in your editor. Next add the following code to the newly created tsconfig file
```
{
"compilerOptions": {
"strict": true,
"target": "ES2020",
"moduleResolution": "node",
"allowJs": true,
"outDir": "HACK_BECAUSE_OF_ALLOW_JS"
}
}
```
For the sake of this course, we will not go into detail about what each of these options does. If you are interested in learning more about the TypeScript compiler options, you can read more about them [here](https://www.typescriptlang.org/tsconfig).
- Next, go ahead and create another file called `dfx.json` with the following command:
```touch dfx.json``` or . Next add the following code to the newly created dfx.json file
<!-- Removed main from dfx.json -->
```
{
"canisters": {
"message_board": {
"type": "custom",
"build": "npx azle message_board",
"root": "src",
"ts": "src/index.ts",
"wasm": ".azle/message_board/message_board.wasm",
"gzip": true
}
}
}
```
the `dfx.json` file is where we will configure our Azle project.
Let us break down this configuration a bit.
- message_board is the name of our canister. We will be using this name to interact with our canister later on in this tutorial.
- The `main` property is the entry point of our canister. This is where we will be writing our code.
- The `type` property tells DFX that we are using a custom canister.
- The `build` property tells DFX how to build our canister. We will be using the Azle CLI to build our canister.
- The `root` property tells DFX where to look for our code.
- The `ts` property tells DFX where to look for our TypeScript code.
- The `wasm` property tells DFX where to look for our compiled wasm file.
- The `gzip` property tells DFX to gzip our compiled wasm file which will reduce the size of our canister.
- Next, go ahead and update your package.json file with the following code and dependencies we would need to build out this project. This can be customized to any setup of your choice.
<!-- removed unecesarry values in package json -->
```
{
"name": "dfinity_project",
"main": "index.js",
"dependencies": {
"@dfinity/agent": "^0.19.2",
"@dfinity/candid": "^0.19.2",
"azle": "^0.18.6",
"uuid": "^9.0.0"
},
"engines": {
"node": "^12 || ^14 || ^16 || ^18"
},
"devDependencies": {
"@types/uuid": "^9.0.1"
}
}
```
- Finally, let us install the dependencies we just added to our package.json file with the following command:
```npm install``` or if you are using yarn ```yarn```
### 2.3 Setting up our smart contract
- With that done, ahead and create a folder called `src` with the following command:
```mkdir src```
or by creating it in your editor.
- Next, let us create a file called `index.js` inside of the `src` folder with the following command:
```touch src/index.ts```
or by creating it in your editor.
This file will be the entry point of our canister. This is where we will be writing our code.
### 2.4 Deep dive into Azle smart contract
Now that we have our Azle project set up, let us begin writing our smart contract. We will be building a simple message board application. This application will allow users to create messages, update messages, delete messages, and list messages.
Azle follows a similar syntax to TypeScript. If you are familiar with TypeScript, you should feel right at home with Azle. If you are not familiar with TypeScript, do not worry, we will be going over the syntax as we go along.
- First, let us import dependencies we will be using in our smart contract. Add the following code to the top of your `index.ts` file:
```
import { query, update, Canister, text, Record, StableBTreeMap, Ok, None, Some, Err, Vec, Result, nat64, ic, Opt, Variant } from 'azle';
import { v4 as uuidv4 } from 'uuid';
```
Let us explain what each of these dependencies does.
- `$query` is an annotation that allows us to query information from our canister.
- `$update` is an annotation that allows us to update information in our canister.
- `Record` is a type that allows us to create a record type.
- `StableBTreeMap` is used for creating a map data structure.
- `Vec` is a type that allows us to create a vector type.
- `match` is a function that allows us to match on a result.
- `Result` is a type that allows us to create a result type.
- `nat64` is a type that allows us to create a 64-bit unsigned integer type.
- `ic` is a type that allows us to create an Internet Computer type.
- `Opt` is a type that allows us to create an optional type.
- `Variant` is a type that allows us to create a variant type.
- `uuidv4` is a function that allows us to generate a unique ID.
- Next, create a type for our message. Add the following code to your `index.ts` file:
```
/**
* This type represents a message that can be listed on a board.
*/
const Message = Record({
id: text,
title: text,
body: text,
attachmentURL: text,
createdAt: nat64,
updatedAt: Opt(nat64)
});
```
- Next, we create a type for our Message Payload. This will be the type of the payload we will be sending to our smart contract. Add the following code to your `index.ts` file:
```
const MessagePayload = Record({
title: text,
body: text,
attachmentURL: text
});
```
- Next, we create an Error type. This will be the type of the error we will be sending to our smart contract. Add the following code to your `index.ts` file:
```
const Error = Variant({
NotFound: text,
InvalidPayload: text,
});
```
- Go ahead and create a storage variable for our message board. Add the following code to your `index.ts` file:
<!-- changed from const to tet -->
```
const messagesStorage = StableBTreeMap(text, Message, 0);
```
This will create a storage variable called `messagesStorage` that will be used to store our messages. We use the `StableBTreeMap` type to create a map data structure. The first argument is the type of the key, the second argument is the type of the value, and the third argument is the size of the key. We use the `text` type for the key because we will be using the message ID as the key. We use the `Message` type for the value because we will be storing messages in our storage variable.
We use the `0` size for the key because we will be using the message ID as the key and the message ID is a string.
With that done, we can dive into the body of our canister.
- First, go ahead and create a default export for our canister. Add the following code to your `index.ts` file:
```
export default Canister({
```
- Next, create a function to get all messages stored in our canister. Add the following code to your `index.ts` file:
```
getMessages: query([], Result(Vec(Message), Error), () => {
return Ok(messagesStorage.values());
}),
```
This function will allow us to get all the messages in our message board. We return an Ok result with the values of our messageStorage variable.
- Next, create a function to get a message by its ID. Add the following code to your `index.ts` file:
```
getMessage: query([text], Result(Message, Error), (id) => {
const messageOpt = messagesStorage.get(id);
if ("None" in messageOpt) {
return Err({ NotFound: `the message with id=${id} not found` });
}
return Ok(messageOpt.Some);
}),
```
This is quite similar with the previous function, the difference is we search for a message by its unique ID, if no message is found, we return an error, otherwise, we return the message.
- Next, go ahead and create a function to create a message. Add the following code to your `index.ts` file:
```
addMessage: update([MessagePayload], Result(Message, Error), (payload) => {
const message = { id: uuidv4(), createdAt: ic.time(), updatedAt: None, ...payload };
messagesStorage.insert(message.id, message);
return Ok(message);
}),
```
This function will allow us to create a message.
We use uuidv4 to generate a unique ID for our message. We use ic.time() to get the current time.
We use the spread operator to spread the payload into our message. Finally, we insert the message into our messageStorage variable and return the message.
- Next, create a function to update a message. Add the following code to your `index.ts` file:
```
updateMessage: update([text, MessagePayload], Result(Message, Error), (id, payload) => {
const messageOpt = messagesStorage.get(id);
if ("None" in messageOpt) {
return Err({ NotFound: `couldn't update a message with id=${id}. message not found` });
}
const message = messageOpt.Some;
const updatedMessage = { ...message, ...payload, updatedAt: Some(ic.time()) };
messagesStorage.insert(message.id, updatedMessage);
return Ok(updatedMessage);
})
```
This function will allow us to update a message.
We first get the message by its ID, if no message is found, we return an error, otherwise, we update the message and return the updated message.
- Finally, go ahead create a function to delete a message. Add the following code to your `index.ts` file:
```
deleteMessage: update([text], Result(Message, Error), (id) => {
const deletedMessage = messagesStorage.remove(id);
if ("None" in deletedMessage) {
return Err({ NotFound: `couldn't delete a message with id=${id}. message not found` });
}
return Ok(deletedMessage.Some);
})
});
```
We use the match function to match on the result of removing a message by its ID. If a message is found, we call the remove function on our messageStorage and return the deleted message. If no message is found, we display an error to the user.
One more thing to note is the uuidV4 package may not work properly in our canister. To resolve this, we need to add the following code at the bottom of our currenct `index.ts` file:
```
// a workaround to make uuid package work with Azle
globalThis.crypto = {
// @ts-ignore
getRandomValues: () => {
let array = new Uint8Array(32);
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
return array;
}
};
```
At the end of this step, your `index.ts` file should look like this:
```
import { query, update, Canister, text, Record, StableBTreeMap, Ok, None, Some, Err, Vec, Result, nat64, ic, Opt, Variant } from 'azle';
import { v4 as uuidv4 } from 'uuid';
/**
* This type represents a message that can be listed on a board.
*/
const Message = Record({
id: text,
title: text,
body: text,
attachmentURL: text,
createdAt: nat64,
updatedAt: Opt(nat64)
});
const MessagePayload = Record({
title: text,
body: text,
attachmentURL: text
});
const Error = Variant({
NotFound: text,
InvalidPayload: text,
});
/**
* `messagesStorage` - it's a key-value datastructure that is used to store messages.
* {@link StableBTreeMap} is a self-balancing tree that acts as a durable data storage that keeps data across canister upgrades.
* For the sake of this contract we've chosen {@link StableBTreeMap} as a storage for the next reasons:
* - `insert`, `get` and `remove` operations have a constant time complexity - O(1)
* - data stored in the map survives canister upgrades unlike using HashMap where data is stored in the heap and it's lost after the canister is upgraded
*
* Brakedown of the `StableBTreeMap(text, Message)` datastructure:
* - the key of map is a `messageId`
* - the value in this map is a message itself `Message` that is related to a given key (`messageId`)
*
* Constructor values:
* 1) text - the type of the key in the map
* 2) Message - the type of the value in the map.
* 3) 0 - memory id where to initialize a map.
*/
const messagesStorage = StableBTreeMap(text, Message, 0);
export default Canister({
getMessages: query([], Result(Vec(Message), Error), () => {
return Ok(messagesStorage.values());
}),
getMessage: query([text], Result(Message, Error), (id) => {
const messageOpt = messagesStorage.get(id);
if ("None" in messageOpt) {
return Err({ NotFound: `the message with id=${id} not found` });
}
return Ok(messageOpt.Some);
}),
addMessage: update([MessagePayload], Result(Message, Error), (payload) => {
const message = { id: uuidv4(), createdAt: ic.time(), updatedAt: None, ...payload };
messagesStorage.insert(message.id, message);
return Ok(message);
}),
updateMessage: update([text, MessagePayload], Result(Message, Error), (id, payload) => {
const messageOpt = messagesStorage.get(id);
if ("None" in messageOpt) {
return Err({ NotFound: `couldn't update a message with id=${id}. message not found` });
}
const message = messageOpt.Some;
const updatedMessage = { ...message, ...payload, updatedAt: Some(ic.time()) };
messagesStorage.insert(message.id, updatedMessage);
return Ok(updatedMessage);
}),
deleteMessage: update([text], Result(Message, Error), (id) => {
const deletedMessage = messagesStorage.remove(id);
if ("None" in deletedMessage) {
return Err({ NotFound: `couldn't delete a message with id=${id}. message not found` });
}
return Ok(deletedMessage.Some);
})
});
// a workaround to make uuid package work with Azle
globalThis.crypto = {
// @ts-ignore
getRandomValues: () => {
let array = new Uint8Array(32);
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
return array;
}
};
```
## 3: Deploy and Interact With Our Canister
Now that we have our canister code, let us build and deploy our canister.
- First, let us start our local Azle replica in the background . Run the following command in your terminal:
```
dfx start --background
```
When you run this command, you should see a similar output:
```
Running dfx start for version 0.14.0
Using the default definition for the 'local' shared network because /Users/<username>/.config/dfx/networks.json does not exist.
Dashboard: http://localhost:49846/_/dashboard
```
## IMPORTANT NOTE
If you make any changes to the StableBTreeMap structure like change datatypes for keys or values, changing size of the key or value, you need to restart dfx with the --clean flag. StableBTreeMap is immutable and any changes to it's configuration after it's been initialized are not supported.
```dfx start --background --clean```
- Next, let us build and deploy our canister. Run the following command in your terminal:
```
dfx deploy
```
When you run this command, you should see a similar output:
```
Creating the "default" identity.
WARNING: The "default" identity is not stored securely. Do not use it to control a lot of cycles/ICP.
To create a more secure identity, create and use an identity that is protected by a password using the following commands:
dfx identity new <my-secure-identity-name> # creates a password protected identity
dfx identity use <my-secure-identity-name> # uses this identity by default
- generating new key at /Users/emmanuel/.config/dfx/identity/default/identity.pem
Your seed phrase:
...
```
If this is your first time running the command, which most likely it is, this may take a bit of time to boot up, so sit back and relax.
Once its done, you should see a similar output:
```
Deployed canisters.
URLs:
Backend canister via Candid interface:
message_board: http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai
```

## 3.1: Interacting with our canister
We have two ways of interacting with our canister. We can either use the command line interface or the web interface. Let us start with the command line interface.
### 3.1.1: Command line interface
- Firstly, let us call our `addMessage` function from our canister file we created earlier in order to add a message to our canister. Run the following command in your terminal:
```
dfx canister call message_board addMessage '(record {"title"= "todo list"; "body"= "some important things"; "attachmentURL"= "url/path/to/some/photo/attachment"})'
```
If this calls successfully, you should get a response similar to this
```
(
variant {
Ok = record {
id = "79daba82-18ce-4f69-afa1-7b3389368d1f";
attachmentURL = "url/path/to/some/photo/attachment";
title = "todo list";
updatedAt = null;
body = "some important things";
createdAt = 1_685_568_853_915_736_000 : nat64;
}
},
)
```
To view the message we just added, we can make use of the candid interface that was generated to us when we ran the "dfx deploy" command.
It should look something like this:
```
http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai
```
Copy the url generated for your canister and open it in your browser.
You should see an interface similar to this:

Click on the `getMessage` function and enter the id of the message you want to get. In this case, we want to get the message we just added, so we enter the id of the message we got from the response of the `addMessage` function. In your case, the id will be different from mine.
Click on the `Call` button and you should see a response similar to this:
```
(
variant {
Ok = record {
id = "79daba82-18ce-4f69-afa1-7b3389368d1f";
attachmentURL = "url/path/to/some/photo/attachment";
title = "todo list";
updatedAt = null;
body = "some important things";
createdAt = 1_685_568_853_915_736_000 : nat64;
}
},
)
```

This makes the interaction with our canister a lot easier.
We can go ahead and call other functions like `getMessages`, `getMessage`, `updateMessage`, and `deleteMessage` in the same way we called `addMessage` using our terminal.
- Update Message : ```dfx canister call message_board updateMessage '("79daba82-18ce-4f69-afa1-7b3389368d1f", record {"title"= "UPDATED TODO LIST TITLE"; "body"= "some important things"; "attachmentURL"= "url/path/to/some/photo/attachment"})'```
where `79daba82-18ce-4f69-afa1-7b3389368d1f` is the id of the message we want to update. In your case, the id will be different from mine.
- Get Messages : ``` dfx canister call message_board getMessages '()' ```
where `getMessages` is the name of the function we want to call, and `()` is the argument we are passing to the function. In this case, we are not passing any argument to the function.
- Delete Message : ``` dfx canister call message_board deleteMessage '("79daba82-18ce-4f69-afa1-7b3389368d1f")' ```
where `79daba82-18ce-4f69-afa1-7b3389368d1f` is the id of the message we want to delete. In your case, the id will be different from mine.
### 3.1.2: Web interface
Just as we used the interface to get the message we just created in the previous section, you can use it add, update, delete, and get messages from your canister as well as call other functions you may add in your cannister.
Finally, you can shut down your local Azle replica by running the following command in your terminal:
```
dfx stop
```
## 4: Conclusion
In this tutorial, we have learnt how to create a canister, build and deploy it, and interact with it using the command line interface and the
web interface. We have also learnt how to use the Azle framework to build canisters. We have also learnt how to use the Result type to handle errors in our canister code.
## Contributing
Contributions are welcome! If you'd like to contribute, feel free to submit a pull request or create an issue on the repository.
## License
This project is licensed under the MIT License.