The Definitive Guide To Building A FullStack dApp
You probably clicked on this tutorial because you have dipped your toes into the exciting world of web3, looked at some intro resources on what web3 is, and have decided to take the next step in becoming a blockchain developer. In this tutorial, we will explore building a complete dApp on the Ethereum network from scratch.
Overview
For the application we’ll be building, we want users that sign up to the dApp, to be able to view a list of learning resources, vote on the resources they like the most, and add their own resources to the dApp. We will start the tutorial by setting up our development environment with Hardhat, which is a development environment that allows us to build smart contracts on Ethereum easily.
After setting up our environment, we’ll build our smart contract in solidity, test that all methods are working in our terminal, deploy the contract, and connect it to our React frontend. This is what our finished demo looks like:
Setting up our development environment
Let’s set up our development with Hardhat. We’ll use the instructions from the hardhat official documentation to set up our project
Installing Hardhat First, navigate to the directory you would like to build your project in, create an empty directory
mkdir resourcedapp
cd resourcedapp
Make sure you are in the resourcedapp
directory, then initialize the empty directory with the npm init
command and follow the instructions on the terminal.
Now let’s install Hardhat and other Hardhat plugin dependencies that will be useful as we follow along in the tutorial.
npm install --save-dev hardhat @nomiclabs/hardhat-ethers ethers @nomiclabs/hardhat-waffle ethereum-waffle chai
It might take a while for everything to be installed because of some Ethereum Javascript dependencies.
Once everything is installed, in our terminal, run the command:
npx hardhat
This is what we get in our terminal after running the command:
Select Create a basic sample project
, leave the default hardhat config root as it is, then select yes
for “Do you want to add a .gitignore” and we can see that our project has been created successfully. let’s take a look at our project structure in the next section.
Project Structure
We have everything ready to go in building our contract, but before that, let’s examine our file structure.
In our contract folder, we have the default Greeter.sol
file that comes from setting up a sample project. We will be deleting it though as we will be creating our contract file for this tutorial where we will be coding our contract.
In the Script folder, we have a sample-script.js
file which can be looked at as our deploy script, because it contains code to compile and deploy the contract we write.
In the test
folder, we have a sample-test.js
file to run our test and make sure everything is running fine before deploying the contract.
Our hardhat.config.js
contains hardhat configurations for our project. Configurations like which account to deploy, which version of solidity is used in our contract, and other configurations that help in smooth dApp development.
Coding our smart contract
In the previous section we looked at the file structure, and now it’s time to begin writing our contract code.
The first thing we do is delete the greeter.sol
file in the contracts
folder and create a new file called Resource.sol
, as this is where we will be writing our smart contract functionalities. Before jumping into the code, let’s look at the functionalities we want to achieve in the smart contract:
- Add a new resource - To do this we will need an
addResource
function, that when invoked will create a new resource and add it to our array of resources. - List all of our resources - To do this, we will need a function
getResources
to list all our resources. - Vote on a resource - We need a
voteResource
function which we will pass in a particular resourceid
and vote on it. - Get a particular resource - We need a
getResource
function that returns a particular resource.
Solidity is an object-oriented language, if you’re familiar with Javascript or Python you won’t have much problem programming in it.
I’ll be pasting the whole contract code in the Resource.sol
file we created earlier and i’ll break it down bit by bit so you see what the contract does.
//Resource.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0; // Solidity files have to start with this pragma.
contract ResourceShare {
struct Resource {
uint8 id;
address creator;
string title;
string url;
string description;
uint8 total_votes;
}
mapping(uint256 => Resource) public resources;
event savingsEvent(uint256 indexed _resourceId);
uint8 public numResource;
constructor() {
numResource = 0;
addResource(
"freecodecamp",
"https://www.freecodecamp.org/",
"Learn to code"
);
}
function addResource(
string memory title,
string memory url,
string memory description
) public {
Resource storage resource = resources[numResource];
resource.creator = msg.sender;
resource.total_votes = 0;
resources[numResource] = Resource(
numResource,
resource.creator,
title,
url,
description,
resource.total_votes
);
numResource++;
}
//return a particular resource
function getResource(uint256 resourceId)
public
view
returns (Resource memory)
{
return resources[resourceId];
}
//return the array of resources
function getResources() public view returns (Resource[] memory) {
Resource\[] memory id = new Resource[\](numResource);
for (uint256 i = 0; i < numResource; i++) {
Resource storage resource = resources[i];
id[i] = resource;
}
return id;
}
// Vote a resource
function voteResource(uint256 resourceId) public {
Resource storage resource = resources[resourceId];
resource.total_votes++;
}
}
Contract Structure
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0; // Solidity files have to start with this pragma.
contract ResourceShare {
}
Solidity contracts generally start with specifying which version of solidity our contract is written in. The pragma
keyword is what instructs solidity of the version we are using, in our case version 0.8.0
. Next, we name our contract ResourceShare
with the curly brace after. All we will be doing has to be in the curly braces.
Resource Struct
A struct
in solidity is just like classes in other languages. It is used to define custom types for an entity and the attributes it might have.
struct Resource {
address creator;
string title;
string url;
string description;
uint8 total_votes;
}
In our case we have a resource struct
with the following attributes:
- id - unique id for a particular resource shared
- creator - the creator of the resource shared
- title - the title of our resource
- url - the url for the resource we are sharing
- description - the description of the resource
- total_votes - the total votes the resource receives
Now that we have our Resource struct
defined, let’s map each of our Resource and code the functionalities for the contract.
Add a Resource
// Resource.sol
mapping(uint256 => Resource) public resources;
uint8 public numResource;
constructor() {
numResource = 0;
addResource(
"freecodecamp",
"https://www.freecodecamp.org/",
"Learn to code"
);
}
function addResource(
string memory title,
string memory url,
string memory description
) public {
Resource storage resource = resources[numResource]; //create new instance of our resource object.
resource.creator = msg.sender;
resource.total_votes = 0;
resources[numResource] = Resource(
numResource,
resource.creator,
title,
url,
description,
resource.total_votes
);
numResource++;
}
The first line of the code in the sample above is a mapping in solidity which is like an object in Javascript used in storing data in a key-value pair format or like a hash map in some other language.
mapping(uint256 => Resource) public resources;
In this line, we map solidity inbuilt type uint256
to our Resource struct
and set to public the resources array. We create a numResource
variable, which we will use to refer to the id
of a resource we have in our resources array
Note 1: In solidity, using a public
keyword means that the specified data can be called by anybody or any contract.
Note 2: The resources
array can be populated by different resources in the image of the Resource struct
we defined.
Our addResource
function accepts three parameters title
, url
, and description
. In solidity, we have to specify the type of parameters we pass into the function, and the memory
keyword to store parameters temporarily in state memory.
Next, we create a new instance of our resource object in our solidity contract storage using the storage
keyword. This will allow us to change the contract state directly. What we do next is set the value of what a particular resource we want to share will have.
We set the creator of the resource to always be the msg.sender
, which in solidity is the person calling the functions in our contract or the person connected to our contract, and the total_votes
to be zero initially. Next, we set all the values of the resource we want to share, including the arguments the user creating the resource will provide and increment numResource
each time the function is called.
We can see a constructor function in the code. The constructor function is called every time we run our contract.
Get all Resources In the previous section, we wrote the function to add a new resource. Let’s look at how to get a particular resource, then all resources in this section.
//Resource.sol
//return a particular resource
function getResource(uint256 resourceId) public view returns (Resource memory) {
return resources[resourceId];
}
//return the array of resources
function getResources() public view returns (Resource[] memory) {
Resource\[] memory id = new Resource[\](numResource);
for (uint256 i = 0; i < numResource; i++) {
Resource storage resource = resources[i];
id[i] = resource;
}
return id;
}
In the code above, the first function getResource
accepts resourceId
as a parameter used to target a particular resource, then returns it. The view
syntax indicates we are not making any changes to the contract state, and only viewing or querying state data. The returns
syntax tells us the type of data we are returning, in our case a type of our Resource struct
.
The second function getResources
loops over all the resources in the array and returns them.
Voting a resource We will make a provision for a voting function, so the user can also vote a particular resource they love.
//Resource.sol
// Vote a resource
function voteResource(uint256 resourceId) public {
Resource storage resource = resources[resourceId];
resource.total_votes++;
}
This function just targets a particular resource in the contract storage and increments the total votes each time the voteResource
function is called.
Deploying our contract
We are done coding our contract, it’s time to deploy it!. In this section, we will deploy the contract twice. First, we will create a local node on our machine and deploy the contract locally, then later on we will deploy the contract live to the polygon matic test network, where anyone in the world can view the contract and the transactions that happen on it. Now let’s begin.
Deploying our contract locally
To deploy our contract locally to a local node network provided by hardhat
, let’s start running the node by running the command:
npx hardhat node
After running this command, Hardhat provides us with 20 accounts each with their wallet account ID and a private key. We will be using those to connect to MetaMask later on. Each account is funded with fake 100ETH that we can use to carry out operations and pay gas fees in the dApp we are building. Here is what our terminal looks like:
As you can see from the image, the port our local network is running on: http://127.0.0.1:8545/.
Navigate to your hardhat.config.js
and add this network configuration.
//hardhat.config.js
module.exports = {
<------>
solidity: "0.8.4",
networks: {
hardhat: {
chainId: 31337
}
}
};
In our script folder, rename the sample-script.js
file to deploy.js
as this is where we will write the script for deploying our smart contract. Tweak the deploy.js
file to look like this:
// deploy.js
const hre = require("hardhat");
async function main() {
// If this script is run directly using `node` you may want to call compile
// manually to make sure everything is compiled
// await hre.run('compile');
// We get the contract to deploy
const ResourceShare = await hre.ethers.getContractFactory("ResourceShare");
const resourceshare = await ResourceShare.deploy();
await resourceshare.deployed();
console.log("resourceshare deployed to:", resourceshare.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Now to finally deploy our contract we run the command
npx hardhat run --network localhost scripts/deploy.js
When we run the above command, Hardhat automatically creates an artifacts folder at the root of our directory. The artifact folder is very important if we want to interact with the smart contract we just deployed and connect to our frontend. This is what we get in our terminal once we have a successful deployment:
we can also see in the terminal tab where we started our node:
Above we can see different information about our deployed contract, including the contract address, the account from the 20 accounts our contract was deployed from, and gas fees used to deploy our contract.
Now that we have a successful deployment, let’s install and connect our metamask wallet to our account so we can interact with the frontend application we will be creating soon.
Connecting to Metamask
I’ll like for us to concentrate on the most important things, so I’ll be linking you to a guide to install and set up your metamask wallet so we can focus on the important stuff of connecting our contract to metamask. Now that you have your metamask wallet installed, let’s connect to our Hardhat local network and import the account we deployed our contract from. Follow these steps:
-
Click on the metamask icon on the extension tab and sign in to metamask. After signing in, you will be automatically directed to your Ethereum mainnet account.
-
Click on the network dropdown and click on the “Add Network” button.
-
Fill in the fields with the following information and save.
-
Make sure you are now on the Hardhat network you just added, then click on the circular icon in the top right corner.
-
Go to your terminal tab where you started the Hardhat node and were provided with 20 accounts. Look for the first account, which is the account your contract was deployed from, then copy and paste it into the field that asks you to paste your private key.
Click on Import and you’ll see the balance for the account you imported.
The balance is no longer 10000Eth as we saw from our terminal because we have used a bit of Eth to pay gas fees for deploying the contract.
Now that we have everything set up, it’s time to build our frontend and integrate it with our contract so the user can perform operations from our UI.
Building our Frontend
In the last section, we looked at how to set up our metamask wallet and import our account. Now is the time to build our frontend and interact with the contract.
We will use create-react-app
to spin off a frontend at the root folder of our application.
npx create-react-app frontend
Now that we have our frontend application set up, let’s navigate to our App.js
file in the src
folder and begin writing some code!.
Before jumping into some code, I’ll like to discuss how our frontend connects to our smart contract and the general structure our frontend will follow.
When building a dApp, there are helpful libraries that make it possible for us to query the blockchain for some data or make changes to a contract state in the blockchain. Examples of such libraries are web3.js, and ethers.js which are the most popular. Without these libraries, interacting with our UI will be such a pain. In our case, we are using the ethers.js
library which we have already installed when starting this tutorial, and that’s why we’ll be importing it in our app.js
file.
Frontend structure We are building a resource sharing dApp, so our React frontend will have two components:
ListResources
- This is a component of a particular resource we share in our application. It will contain all the information of the resource we share.CreateResource
- This component contains a form, so we can add a new resource to our app.
These two components will be imported into our App.js
file.
Let’s look at what our App.js
looks like:
// App.js
import './App.css'
import ResourceArtifact from '../src/artifact/contracts/Resource.sol/ResourceShare.json'
import { ethers } from 'ethers'
import ListResources from './components/ListResources'
import CreateResource from './components/CreateResource'
import { useEffect, useState } from 'react'
const resourceAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
function App() {
const [resourceData, setResourceData] = useState([])
const [toggleModal, setToggleModal] = useState(false)
const [contract, setContract] = useState()
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' })
}
function addResource() {
setToggleModal(!toggleModal)
}
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner()
async function _intializeContract(init) {
// We first initialize ethers by creating a provider using window.ethereum
// When, we initialize the contract using that provider and the token's
// artifact. You can do this same thing with your contracts.
const contract = new ethers.Contract(
resourceAddress,
ResourceArtifact.abi,
init,
)
return contract
}
useEffect(() => {
// in this case, we only care to query the contract when signed in
if (typeof window.ethereum !== 'undefined') {
(async function getResourcesCount() {
await requestAccount()
const contract = await _intializeContract(signer)
const resourcedata = await contract.getResources()
const resources = [...resourcedata]
setContract(contract)
setResourceData(resources)
})()
}
}, [])
return (
<>
<header>
<button onClick={addResource}>Add a resource</button>
</header>
<main>
<CreateResource toggleModal={toggleModal} contract={contract} />
<section className="resources">
{resourceData.map((resource, id) => {
return (
<div key={id}>
<ListResources resource={resource} contract={contract} />
</div>
)
})}
</section>
</main>
</>
)
}
export default App
let’s analyze our App.js
file bit by bit starting with the first 8 lines:
import './App.css'
import ResourceArtifact from '../src/artifact/contracts/Resource.sol/ResourceShare.json'
import { ethers } from 'ethers'
import ListResources from './components/ListResources'
import CreateResource from './components/CreateResource'
import { useEffect, useState } from 'react'
const resourceAddress = '0x5FbDB2315678afecb367f032d93F642f64180aa3'
Above, we imported the ethers.js library we talked about earlier, our components, our contract articfact file, and our contract address.
To connect and interact with our contract, it’s a must to have the contract address and the ABI file which can be found in our artifact folder. The ABI file contains important information about our contract like the methods to call in our contract.
const [resourceData, setResourceData] = useState([])
const [toggleModal, setToggleModal] = useState(false)
const [contract, setContract] = useState()
async function requestAccount() {
await window.ethereum.request({ method: 'eth_requestAccounts' })
}
Here is our application state.
- The
resourceData
state will store the resource array containing all the resources. - The
toggleModal
state to hold state to decide if our modal is open or not. - The
contract
state so we can store the contract and pass it as a prop, so we can call the contract methods from other components.
Then we have the requestAccount
function that calls the eth_requestAccounts
method on the ethereum window object. When this method is called, the user is prompted to sign into their metamask wallet on the browser.
const provider = new ethers.providers.Web3Provider(window.ethereum)
const signer = provider.getSigner()
async function _intializeContract(init) {
// We first initialize ethers by creating a provider using window.ethereum
// When, we initialize the contract using that provider and the token's
// artifact. You can do this same thing with your contracts.
const contract = new ethers.Contract(
resourceAddress,
ResourceArtifact.abi,
init,
)
return contract
}
useEffect(() => {
// in this case, we only care to query the contract when signed in
if (typeof window.ethereum !== 'undefined') {
(async function getResources() {
await requestAccount()
const contract = await _intializeContract(signer)
const resourcedata = await contract.getResources()
const resources = [...resourcedata]
setContract(contract)
setResourceData(resources)
})()
}
}, [])
In the code above, we have an *_intializeContract*
function that initializes our contract with the ethers.js library. For us to initialize the contract, we need to create a contract variable that uses the Contract
method from Ethers library, which accepts three parameters:
- The address.
- The
ResourceShare
contract artifact ABI - The signer account that we get using
window.ethereum
as our provider.
Then we return the contract. We will see how the _intializeContract function would be used later.
In our React useEffect
hook, we call the getResources
function in an IIFE, that calls the requestAccount()
function prompting the user to sign in with their Metamask, then we initialize our contract into a contract
variable and call the getResources
method in our contract, and set our contract
and resourceData
state.
return (
<>
<header>
<button onClick={addResource}>Add a resource</button>
</header>
<main>
<CreateResource toggleModal={toggleModal} contract={contract} />
<section className="resources">
{resourceData.map((resource, id) => {
return (
<div key={id}>
<ListResources resource={resource} contract={contract} />
</div>
)
})}
</section>
</main>
</>
)
Here we return some markup of what we will display in our UI. In our header, we only have an “Add a resource” button, when clicked will call the addResource
function which toggles our modal and reveals a form for us to create and add a new resource.
In the main
tag, we have the CreateResource
component, then we loop through our resourceData
array and pass in the resource object into our ListResources
component as a prop. We also pass in the contract as a prop in the two components so we can call the contract methods in the two components.
List resource Component
We looped through the resource array state in our App.js
component and supplied the object prop to our ListResources
component. Let’s look at what our ListResources
looks like:
function ListResources({ resource, contract }) {
async function vote(id) {
await contract.voteResource(id)
}
return (
<div className="project">
<h2>{resource.title}</h2>{' '}
<span className="creator">{resource.creator}</span>
<h3>description:</h3>
<p>{resource.description}</p>
<h4>Link:<a href={resource.url}>{resource.url}</a></h4>
<h4>Votes: {resource.total_votes}</h4>
<button
onClick={(id) => vote(resource.id)}
>
Vote
</button>
</div>
)
}
export default ListResources
In this component, we just accept the resource
and contract
prop, list display all the data in our resource object. When we click on the button we call a vote
function that accepts the resource ID and calls the voteResource
method in our contract incrementing the total_votes
count by 1.
Create Resource Components Our create resource components is a form modal with input fields, to add a new resource.
import React, { useState } from 'react'
function CreateResource({ toggleModal, contract }) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [url, setUrl] = useState('')
const handleSubmit = (event) => {
event.preventDefault()
contract.addResource(title, url, description)
}
return (
<div>
{toggleModal === true && (
<div className="addresource">
<form onSubmit={handleSubmit}>
<label>
Enter resource title:
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</label>
<label>
Enter resource url:
<input
type="text"
value={url}
onChange={(e) => setUrl(e.target.value)}
/>
</label>
<label>
Enter resource description:
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</label>
<input type="submit" className="submit" />
</form>
</div>
)}
</div>
)
}
export default CreateResource
When the toggleModal
state is true
, this component renders conditionally. We have a state for the resource title
, description
, and url
, and we set the state to the input value whenever our input changes. The handleSubmit
function is invoked when the form is submitted, and our addResource
method is called from the contract.
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
Deploying our contract live on a test network
In the last section, we looked at how to connect our frontend to our smart contract. In this section, we will look at how to deploy our smart contract to a live test network for everyone to see. The network we will be deploying to is the Ropsten Ethereum test network, which allows us to deploy our contract and testing it out before deploying it to the Ethereum mainnet.
First, we have to click on the metamask extension icon, then click the network dropdown and select the Ropsten test network
. We need a way to interact with the Ropsten test network without running a local Ethereum node on our machine, and there are quite a few services that allow us to do that, one of them being Infura. To use the Ropsten network Ethereum node provided by Infura, we need to create an account and sign in. When signed in, click on “create a new project”
After clicking, you’ll be prompted by a modal asking for the project name.
We can name our project whatever we like and click the create button. In our project dashboard, we can see our project details and the endpoints we will be using.
Click on the endpoint dropdown and select Ropsten network. Now we have our project ID and the Ropsten endpoint which we will need to connect to the Ropsten network. Let’s configure Hardhat to deploy to the network. In our hardhat.config.js
, we now have:
module.exports = {
paths: {
artifacts: './frontend/src/artifact',
},
solidity: "0.8.4",
networks: {
hardhat: {
chainId: 31337
},
ropsten: {
url: "https://ropsten.infura.io/v3/ca9b0c1b342b44658282a52daaf1be25",
accounts: [`0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80`]
}
}
We have added a new configuration to our networks. The Ropsten network configuration requires a URL
which is our ropsten endpoint gotten from Infura, and an account
which is the private key of the Ropsten wallet account we selected on metamask.
To get the private key of the account, click on account details, then on “export private key”
Type in your metamask password and click confirm, then you can copy the private key and past it in your hardhat configuration. Just make sure you prefix it with 0x
as seen in our Ropsten configuration
In a real-world use case where you’ll be deploying to the Ethereum mainnet, make sure the private keys are private and are stored where you can only access to prevent loss of funds. You can store it in an
env
file if you’d like.
We are now set to deploy the contract on the Ropsten network. All we need to do now is fund our Ropsten test network account and run the Hardhat deploy command. To fund our account, we need to paste our Ropsten testnet wallet account into a Ropsten faucet site that sends us free Ether.
Wait a bit, and you will see the free Ether sent to you
Now we can run the deploy command:
npx hardhat run --network ropsten scripts/deploy.js
and our contract is now deployed live on the Ropsten network and you can check it out on Etherscan, a site that allows you to explore the contract you just deployed.
To explore our contract on Etherscan, copy the account the contract is deployed to, from the terminal and paste it on Etherscan.
Click on the search icon and you will see information about the contract we deployed.
Yay!. We have concluded the tutorial and you can find the source code here.
Conclusion
Finally, we are done with this tutorial. We learned how to build a dApp from start to finish, connect the smart contract and deploy it on a live test network, while exploring tools like Infura and Etherscan. I hope this tutorial gets you up and running building dApps on the Ethereum blockchain.