Skip to main content

Write

Beginner
Concept

ICP supports a wide range of applications and architecture types. Apps can range from a single canister to complex, multi-canister projects and everything in between.

You can begin writing and structuring your application using one of two primary workflows:

  • Standard workflow: The developer writes both the frontend and backend code, then deploys both to ICP as canisters.

  • Framework-based workflow: An external framework is used to help facilitate creating and deploying canisters. Learn more about frameworks.

Standard workflow

Choosing the programming language for the backend

The backend stores the application’s data and contains the core logic. Several languages are supported, such as:

  • Rust: Supported by DFINITY. Currently, Rust is the language with the most production coverage for ICP applications. All system smart contracts, such as the DAO governing ICP, the ICP ledger, and the Bitcoin and Ethereum integration smart contracts, are written in Rust. This language gives the developer full control over all aspects of the smart contract, starting from performance to memory management. The only disadvantage of Rust is that it is lower-level compared to other languages and requires more expert programming skills to write safe and secure code. Learn more about using Rust.

  • Motoko: Supported by DFINITY. Motoko is production-ready and was specifically designed to onboard developers onto ICP and leverage the actor-based programming model of ICP. It is a high-level language with a garbage collector and syntax that is similar to TypeScript. Examples of production smart contracts that use Motoko include ICDex and CycleOps.Learn more about using Motoko.

  • TypeScript (beta): Supported by Demergent Labs under the name Azle. Azle is in beta. Please check the Azle website for more information.

  • Python (beta): Supported by Demergent Labs under the name Kybra. Kybra is in beta. Please check the Kybra website for more information.

  • C++: Supported through the C++ CDK.

Choosing a web framework for the frontend

The HTTP Gateway protocol of ICP allows browsers to load web assets such as JS, HTML, and CSS from a canister via HTTP. This means that web assets can be stored fully onchain and developers don’t need to use traditional centralized web hosting to serve the UI of their application.

Svelte, React, and Vue have been used successfully in production. dfx v0.17.0 and newer can be used to generate project templates that include one of these frameworks. Learn more.

The typical development workflow of the frontend is:

  1. The developer writes frontend code such as HTML, JS, or CSS.
  2. The developer configures their dfx.json file to include a frontend canister with type "assets".
  3. The developer deploys the project. dfx will compile the frontend asset files into an asset canister.
  4. Users open the application in the browser by navigating to the URL https://<canister-id>.icp0.io or a custom domain if one has been registered for the canister.
  5. The canister serves the web assets to the browser via its http_request endpoint that gets invoked for each HTTP request.
  6. When the JS code runs in the browser, it can call the backend canister endpoints using the ICP JavaScript agent library, which is analogous to web3.js and ethers.js of Ethereum.

Limitations

Server-side rendering (SSR) does not work in canisters because they require JS code that is not built into canisters. In the future, this might become possible with Azle. Until then, if SSR is required, then one solution is to host the frontend outside of ICP while keeping the core logic in the backend canister.

Having no frontend at all is also a valid option for smart contracts that don’t have a UI and are callable only by users or other smart contracts.

Creating a new project

Install the IC SDK.
Download and install an IDE or code editor. VS Code is recommended.

For writing Motoko code, the Motoko VS Code extension is highly recommended for syntax highlighting.

Create a new project. When prompted, select your backend language and frontend framework of choice:

dfx new hello

The dfx new command creates a new project directory, template files, and a new <project_name> Git repository for your project.

You can also obtain projects from other sources, such as ICP Ninja or the sample repository.

When creating new projects with dfx new, only alphanumeric characters and underscores should be used. This is to assure that project names are valid within Motoko, JavaScript, and other contexts.

Navigate into your project directory:

cd hello

For projects created with dfx new, the project structure will resemble the following. If you are using an ICP Ninja project or other sample project, project structure may vary.

hello/
├── README.md # Default project documentation
├── dfx.json # Project configuration file
├── node_modules # Libraries for frontend development
├── package-lock.json
├── package.json
├── src # Source files directory
│ ├── hello_backend
│ │ └── main.mo
│ ├── hello_frontend
│ ├── assets
│ │ ├── logo.png
│ │ ├── main.css
│ │ └── sample-asset.txt
│ └── src
│ ├── index.html
│ └── index.js
└── webpack.config.js

In this directory, the following files and directories are notable:

  • README.md: The default README file to be used for documenting your project.
  • dfx.json: The default ICP configuration file used to set configurable options for your project.
  • src/: The source directory that contains all of your dapp's source files.
  • hello_backend: The source directory that contains your dapp's backend code files.
  • hello_frontend: The source directory that contains your dapp's frontend code files.

Reviewing the default program code

Open the backend canister source code file in your code editor. The backend canister's code will be located in the src/hello_backend subdirectory. For projects created with dfx new, the default backend code will resemble the following. If you are using an ICP Ninja project or other sample project, program code will vary.

src/hello_backend/main.mo
actor {
public query func greet(name : Text) : async Text {
return "Hello, " # name # "!";
};
};

Framework-based workflow

Juno

Juno is a community project that is tailored for Web2 developers. It takes care of hosting code and data in canisters such that developers can write Web3 applications using familiar Web2 concepts and patterns. For more details, please follow the official Juno documentation.

Bitfinity EVM

Bitfinity EVM is tailored for Solidity developers. It is a canister that runs an instance of the Ethereum virtual machine and allows developers to upload and execute smart contracts written in Solidity. For more details, please follow the official Bitfinity documentation.

Architecture considerations

A common question when developing an application is how and where to store the data. In contrast to traditional platforms, ICP does not provide a database. Instead, ICP can persists changes in the canister state using stable memory. This means that developers have a lot of freedom in organizing and storing the data. The recommended practice is to use already existing libraries, such as the Motoko stable regions library or the Rust stable-structures library, to store data in the stable memory.

Another question that developers should ask is how to structure their application. It is possible to build an application consisting of multiple canisters that communicate with each other. A common pitfall for new developers is designing the application for millions of users from the get-go without understanding the underlying trade-offs of the system. It is better to start with the simplest possible architecture and iteratively improve it with user growth.

Canister per service architecture

Canisters can be thought of as microservices, where each canister is responsible for a specific service of the application, such as managing users, storing data, or processing data. Note that all benefits and disadvantages of the traditional microservice architecture apply here as well. The default project structure that dfx new generates can be viewed as the simplest microservice architecture, with the frontend canister being responsible for serving web assets and the backend canister being responsible for the core logic and of the application.

Single canister architecture

A single canister can host the entire application stack, including its web assets, core logic, and data. To write a single canister that hosts frontend assets and backend core logic, you will need to use a library for the asset storage API, such as the ic-certified-assets library for Rust canisters. A few examples of single canister projects include:

Even though this architecture is simple, it can scale to thousands of users and gigabytes of data.

Canister per subnet architecture

ICP scales horizontally via subnets, so applications can also scale by utilizing more subnets. One way to achieve this is to have one or multiple canisters per subnet and then shard data over these canisters to distribute the load. This is the most scalable architecture and could, in theory, support millions of users and terabytes of data. Since the application data and logic are distributed over multiple subnets, this requires expert knowledge of distributed programming. The cost of development and maintenance is much higher compared to the single-canister architecture.

Canister per user architecture

This architecture is based on the vision that Web3 users should have full control over their data. The idea is to create a canister per user and make the user the controller of their canister. The main canister of the application would then orchestrate user canisters to implement the application’s functionality. Since users are controllers of their canisters, they can install their own code, decide how to participate in the application, and determine what data to share. These user benefits come at large development costs because the main canister needs to be programmed in such a way that it can handle all possible actions of potentially malicious user canisters. This is a new and unprecedented way of programming. There hasn’t been a successful implementation of this vision yet. A couple of projects that opted for this architecture, but only NFID Vaults have given the ownership of canisters to the users. A common misconception is that the canister-per-user architecture is the most scalable; actually, canister-per-subnet is more performant because it can utilize multiple subnets without having the overhead of too many canisters.

Multi-canister architecture samples