Building a Front-End Dapp on the Internet Computer
An approachable walkthrough for blockchain developers and engineers who are used to hosting static assets on Netlify, Fastly, or S3 buckets.
By Kyle Peacock, Software Engineer | DFINITY
The Internet Computer blockchain enables developers to build scalable web experiences using canister smart contracts, entirely on public blockchain, and serve them directly to end users via web browsers.
This post provides a walkthrough of the process of building a front-end dapp on the Internet Computer while taking advantage of the new features it supports. If it seems simple, that’s great! We worked to make this workflow approachable for blockchain developers, and especially engineers who are already used to hosting static assets on Netlify, Fastly, or S3 buckets. By the end of this tutorial, your dapp or website will join more than 1,000 other websites already running on the Internet Computer.
For the sake of this demo, I’ll be using Gatsby.js.
Getting started
To begin, I run the command npm init gatsby
. I’m prompted with, “What would you like to call your site?” I’ll call this “contact_book.” I’ll use that name for the corresponding folder as well.
Next, I’m asked if I’ll be using a CMS, and I answer, “No.” I’ll select “styled-components” from the styling system prompt as a preference, skip the other optional features, and then we’re on our way with a fresh project!
This will set me up with a simple file structure:
├── README.md
├── gatsby-config.js
├── package-lock.json
├── package.json
└── src
├── images
│ └── icon.png
└── pages
├── 404.js
└── index.js
Deploy a static site
We can start the project up with webpack-dev-server by running npm run develop
, and we can compile the project into static HTML, CSS, and JavaScript assets with the command npm run build
.
In order to host the project on the Internet Computer, we will need to do the following:
- Create and configure a dfx.json file at the root of the project
- Install the dfx SDK
- Deploy using the dfx deploy command
Creating dfx.json
Because Gatsby compiles its build output into the public directory, our dfx.json file will look like this:
// dfx.json
{
"canisters": {
"www": {
"type": "assets",
"source": ["public"]
}
}
}
Installing dfx
Follow the instructions here to install the SDK: https://internetcomputer.org/docs/current/developer-docs/build/install-upgrade-remove/
It supports Mac, Linux, and Windows using WSL or VirtualBox.
Deploy your site
Start by running npm run build
to compile your website. You can now run dfx deploy --network ic --no-wallet
from the same directory as your dfx.json to publish your website to the Internet Computer.
Once your site deploy is finished, you can find your canister id by running dfx canister id www
, and then navigating to https://{canisterId}.ic0.app.
How to fund your first canister
We now have a guide on how to pay for and set up your first canister. The basic flow (for now) basically looks like this:
- Use ICP in the NNS front-end dapp to create a canister
- Link that canister to your dfx principal
- Deploy to the existing canister
- Top off the canister with cycles using the NNS interface when needed
Take a breather
Congratulations — you’ve deployed your first Internet Computer web dapp! There’s a very good chance that this is your first dapp built on blockchain technology, and that’s worth celebrating. You’ll see that all your assets from HTML to images are all behaving normally, as though you had pulled them directly from an old-school Nginx or PHP static server.
Customizing the dapp
Now let’s customize the code here a bit. I want to build a contact book, so let’s swap out the logic in src/pages/index.js with our new dapp logic.
Basically, we have a form that can allow a user to create a contact, an input to search for contacts by email address, and a component to render a saved contact.
There are any number of ways that we can persist this information. I might initially start by writing the data to localStorage, Firebase, or MongoDB Atlas as simple text, encoded using JSON.stringify(). Today, I’ll persist that data using a canister smart contract on the Internet Computer.
Adding a back end
We’ll need to make a few changes to modify our project to add a back-end canister.
- Add the source code for our contract
- Configure dfx.json for the back-end canister
- Configure Gatsby to alias our code generated by dfx
- Use the @dfinity/agent npm package to make calls to the back end with an Actor
- Connect the Actor to our dapp logic
Adding our back-end logic
I’ll create a Motoko canister that will implement the simple logic of setting and getting information, stored in a HashMap.
// Main.mo
import HM "mo:base/HashMap";
import Text "mo:base/Text";
import Error "mo:base/Error";
import Iter "mo:base/Iter";
actor {
stable var entries : [(Text, Text)] = [];
let store: HM.HashMap<Text, Text> = HM.fromIter(entries.vals(), 16, Text.equal, Text.hash);
/// returns null if there was no previous value, else returns previous value
public shared func set(k:Text,v:Text): async ?Text {
if(k == ""){
throw Error.reject("Empty string is not a valid key");
};
return store.replace(k, v);
};
public query func get(k:Text): async ?Text {
return store.get(k);
};
system func preupgrade() {
entries := Iter.toArray(store.entries());
};
system func postupgrade() {
entries := [];
};
};
Without diving too far into the details here, this code uses a stable var to persist our data across upgrades, and initializes a friendly HashMap interface that stores data with a Text type key and a Text type value.
We then implement our set and get interfaces, and add preupgrade and postupgrade hooks, again to persist data across upgrades.
I’ll save this to a new folder in my src directory, at src/backend/contact_book/Main.mo. This code is written in Motoko, a language maintained by Dfinity to specifically cater to the features of the Internet Computer. The IC supports any language that can compile to Web Assembly, and Rust and C are other popular choices for canister development. Motoko is an open-source language, and you can learn more about it here.
Configure dfx.json
Now we need to configure dfx to be aware of our new canister. We’ll add a new canister object for it, and we will link it as a dependency for our front-end canister. That looks something like this:
// dfx.json
{
"canisters": {
"contact_book": {
"main": "src/backend/contact_book/Main.mo"
},
"www": {
"dependencies": ["contact_book"],
"type": "assets",
"source": ["public"]
}
}
}
Configure Gatsby
Next, we’ll need to update Gatsby with an alias that points to dynamically generated code from dfx that will be located in a hidden .dfx folder in your project. We’ll create a gatsby-node.js file in the root of our project, and write some code that will use our settings in dfx.json to import our custom interfaces for our new back end.
// gatsby-node.js
const dfxJson = require("./dfx.json");
const webpack = require("webpack");
const path = require("path");
const aliases = Object.entries(dfxJson.canisters).reduce(
(acc, [name, _value]) => {
// Get the network name, or `local` by default.
const networkName = process.env["DFX_NETWORK"] || "local";
const outputRoot = path.join(
__dirname,
".dfx",
networkName,
"canisters",
name
);
return {
...acc,
["dfx-generated/" + name]: path.join(outputRoot, name + ".js"),
};
},
{}
);
exports.onCreateWebpackConfig = ({ stage, actions }) => {
actions.setWebpackConfig({
resolve: {
alias: aliases,
},
plugins: [
new webpack.ProvidePlugin({
Buffer: [require.resolve("buffer/"), "Buffer"],
}),
],
});
};
Additionally, we’ll add a proxy to gatsby-config.js file, proxying localhost:8000, which is the default address for our dfx replica.
// gatsby-config.js
module.exports = {
siteMetadata: {
title: "contact book",
},
plugins: ["gatsby-plugin-styled-components"],
proxy: {
prefix: "/api",
url: "http://localhost:8000",
},
};
Using @dfinity/agent
Now that we’ve aliased our dfx generated resources, we can import them and use them in our codebase. So next I’ll create src/actor.js and import @dfinity/agent from dfx-generated/contact_book.
// actor.js
import { Actor, HttpAgent } from "@dfinity/agent";
import {
idlFactory,
canisterId,
} from "dfx-generated/contact_book";
const agent = new HttpAgent();
const actor = Actor.createActor(idlFactory, { agent, canisterId });
export default actor;
Here we create an agent and pass it to an Actor constructor, along with the idlFactory and canisterId from our code generated from the back-end interface.
Then, we export our Actor, which has two methods (set and get) that are already configured with a promise-based API to make type-safe calls to our canister back end.
Finally, we wire it up
We’ll modify our index.js page with logic to store submissions from the form on our canister, using the email field.
I’ll import the Actor (doing this as a dynamic import to avoid initializing the HttpAgent during server-side rendering for Gatsby).
React.useEffect(() => {
import("../actor").then((module) => {
setActor(module.default);
});
}, []);
We’ll use our set method during handleSubmit
to store the data and then clean up our contact form.
actor?.set(email, JSON.stringify(card.toJSON())).then(() => {
alert("card uploaded!");
inputs.forEach((input) => {
input.value = "";
});
setImage("");
});
And then we will use the get method to fetch contacts using the email search.
actor?.get(email).then((returnedCard) => {
if (!returnedCard.length) {
return alert("No contact found for that email");
}
setCard(vCard.fromJSON(returnedCard[0]));
console.log(returnedCard);
});
And now we have a fully functioning dapp that we can run on the Internet Computer!
Wrapping up
Now that we’ve adapted our codebase, our project structure looks like this:
├── README.md
├── dfx.json
├── gatsby-config.js
├── gatsby-node.js
├── package-lock.json
├── package.json
└── src
├── actor.js
├── backend
│ └── contact_book
│ └── Main.mo
├── images
│ └── icon.png
└── pages
├── 404.js
└── index.js
We can test the changes locally by running dfx deploy
. This will build and upload our back end to the local replica. Once that completes, we’ll be able to run our front end using npm run develop -- --port 3000
again, using all of the nice development features such as hot module reloading. We’re specifying a port since Gatsby also defaults to port 8000.
If all goes well, you should be able to test the dapp locally by submitting and then retrieving a contact using the UI.
And that should be it! You can try these steps yourself, download this example project from https://github.com/krpeacock/ic-vcf-gatsby, or use this guide as a reference to get started with your own projects. We can’t wait to see what you build!
____