Bootstrapping the dApp Development Experience

Building dApps with Node.js and Webpack

A step-by-step tutorial to building the back-end for Apps

Muhammad Yahya
OLI Systems
Published in
12 min readJul 27, 2020

--

Blockchain is a relatively young technology that is under continuous development. Similarly, applications built on the top of the blockchain are also still growing. As a result, there are several tools and best practices to streamline the development process. This tutorial will guide you through the steps to build a node.js back-end for a Decentralized Application (dApp). The tutorial introduces different developer tools and best practices and demonstrates how can we use them to boost the development experience.

The Big Picture

A Decentralized Application (dApp) is a kind of web application whose back-end runs on a decentralized network such as Blockchain. The dApp has a traditional front-end built with HTML, CSS, and JavaScript. However, instead of connecting with a backend server, the dApp is connected to a blockchain node. The backend code of the dApp is written in the form of a smart contract using the Solidity programming language. The smart contract is a piece of program that contains the core logic of a dApp and is an integral part of the blockchain. The smart contracts are deployed using a locally running blockchain node and resides on the blockchain network.

The communication between the front-end and the back-end is done through a Web3 JavaScript library. The Web3 is a collection of libraries which contains functionality to interact with the Ethereum ecosystem. The communication is essentially involved reading and writing the data from the blockchain through the smart contracts. A Web3 provider can be set up in both the frontend and the backend to send the transaction and listen to the blockchain events.

The Goal of the Tutorial

The goal of the tutorial is to define a project structure to built a back-end for the dApp using node.js, truffle, and webpack. We will write a simple smart contract that will send some info to the blockchain and emit an event in the response of a successful transaction.

The back-end will perform two main functions:

  1. Publish transactions to the blockchain network
  2. Listen to the blockchain events

Developer Tools

We will make use of the following tools:

Truffle Framework

We are using Truffle framework for the development of smart contracts. The Truffle provides a suite of tools to write smart contracts in Solidity, perform unit tests using Mocha and Chai, and deploy contracts on the Blockchain. Some of the most useful and frequently used truffle commands are:

  • Compile — truffle compile
  • Migrate — truffle migrate
  • Test contracts — truffle test

You can find more from the official docs

Ganache

We will make use of ganache to set up a private blockchain for the smart contract development, testing, and to speed up the overall iteration process. Furthermore, ganache comes with a built-in block explorer to examine blocks, transactions, contracts, and events to gain insight about what’s happening under the hood.

Solidity Prettier Plugin

Solidity Prettier automatically formats the Solidity files according to a predefined style guide.

Using Solidity prettier in combination with Solhint is quite effective as the prettier automatically fix many of the errors that Solhint finds, such as indentation and code styles.

Linter — Solhint

The Solhint is a great tool and library for the static analysis of Solidity code. Using Solhint will help us in automatic syntax checking and detect security vulnerabilities of the Solidity code.

Alternatively, you can use Ethlint.

Truffle Unit Test

We will use the truffle unit testing to test the core functionality of our smart contract. Under the hood, Truffle uses the Mocha testing framework and Chai assertion library to write the tests in JavaScript.

Solidity Code Coverage

The Solidity coverage tool shows how much of the code-base has been covered and tested by the unit tests.

Gas Cost Estimation

We will use ETH Gas Reporter to calculate gas and cost for the execution of functions and contract deployment. The market price data is fetched from Coin Market Cap and gas price is fetched from Eth Gas Station.

Prerequisites

Install the following dependencies globally:

  • Truffle >= v5.0.41 ($ npm i truffle -g)
  • Node >= v10.16.3

We will be using the CoinGecko free API to fetch the cryptocurrency prices.

Project Structure

We will start with creating a new empty project directory dApp-nodeJS-webpack and inside the directory, we will initiate a npm project.

  • In your terminal type the following commands:
$ mkdir dApp-nodeJS-webpack && cd $_
$ npm init

This will create a package.json file that holds the metadata about the project such as project dependencies, purpose, version, and scripts, etc.

  • Create a new file index.js in the root directory. This file will call the overall functionality of the app by requiring the other modules.
$ touch index.js

Add the following log statement to the newly created file:

console.log('Hello world!')
  • To execute the functionality from the index.js file, add the following command to the scripts section of package.json file:
"scripts": {
"app": "node index.js"
},

Now open a terminal inside the root directory and type the following command:

npm run app

If you see the “Hello world!” in the console, you are good to go!

  • Inside the root directory, create a new directory app and inside that initiate a truffle project:
$ mkdir app && cd $_
$ truffle init

The result will look like the following terminal output:

At this point, your project should resemble to this GitHub commit.

Smart Contract Development

For the demo purpose, we are going to develop a simple Registration smart contract. The contract will allow the owner to register new users and only registered users will be able to send their values to the blockchain. Additionally, events will be emitted in the response of each successful transaction for user registration and send values.

The Anatomy of the Smart Contract

Let’s create a new file Registration.sol inside the contracts directory. The smart contract will start with specifying the version pragma. The pragma will specify the compiler version and is important to avoid the compatibility issues:

pragma solidity >=0.5.0 <0.7.0;

Next, we can start by defining the contract itself. The first state variable we have to declare is the address to store the Ethereum address of the contract owner. We have specified the visibility of the variable as public to let other contracts to access it. We will assign the deployer address to the owner using constructor. The constructor will be executed only once at the time when the contract is created.

contract Registration {
address public owner;
constructor() public {
owner = msg.sender;
}
}

We create a struct User that has a bunch of attributes with which we will register a user.

struct User {
string name;
uint8 age;
bool registered;
}

We will declare two events NewUser and NewValue. These events will be triggered once the functions registerUser and send are executed, respectively.

event NewUser(address userAddress, string name, uint8 age);
event NewValue(address userAddress, uint256 value);

The contract will use a mapping users to assign Ethereum address to User struct.

mapping(address => User) public users;

We define a modifier onlyOwner to check if the function executes as expected or not. The use of onlyOwner modifier will allow only owner of the contract to register the new user.

modifier onlyOwner {
require(msg.sender == owner, "Only owner can register a user.");
_;
}

The contract contains two functions, registerUser and send. The registerUser will register a new user with the information specified in the User struct and bind the Ethereum address of the user with the struct. An event NewUser will be emitted if the function execution goes successful.

function registerUser(
address _address,
string memory _name,
uint8 _age
) public onlyOwner {
users[_address] = User(_name, _age, true);
emit NewUser(_address, _name, _age);
}

The send function will allow only registered users to send some random values to the blockchain by emitting an event on a successful transaction.

function send(uint256 _value) public {
require(
users[msg.sender].registered == true,
"Caller is not registered."
);
emit NewValue(msg.sender, _value);
}

Solidity Linting and Prettier

To boost the smart contract development experience, we will use Solidity linting and prettier tools. For dependency management, we will use node and npm. To do that, we will init a npm project inside the app directory using the command npm init. After that run the following command to install the solhint, prettier and prettier-plugin-solidity:

$ npm install solhint --save-dev
$ npm install --save-dev prettier prettier-plugin-solidity

Next, we need to add the following script to package.json file located inside the app directory to run the prettier on all the contracts inside the contracts directory:

"prettier": "prettier --write **/*.sol"

After that, run the following command to apply prettier to our smart contracts:

$ npm run prettier

The output will look like the following:

> prettier --write **/*.solcontracts/Migrations.sol 246ms
contracts/Registration.sol 199ms

For linting, we can use a .solhint.json file to configure Solhint linter for the whole project. Type the following command to generate a sample .solhint.json file:

$ npx solhint --init

Populate the newly created JSON file with the following rulesets:

{
"extends": "solhint:recommended",
"plugins": [],
"rules": {
"avoid-suicide": "error",
"avoid-sha3": "warn"
}
}

Finally, add the following script to the package.json file:

"slint": "solhint \"contracts/**/*.sol\""

Run the following command to apply the linting:

$ npm run slint

The output of the above will look be like this:

> solhint "contracts/**/*.sol"
contracts/Migrations.sol
6:20 warning Variable name must be in mixedCase var-name-mixedcase
✖ 1 problem (0 errors, 1 warning)

Truffle Unit Tests

Unit tests are an integral part of the development to test if a smart contract works as expected or not. We will write a truffle unit test in JavaScript. Under the hood, truffle uses Mocha testing framework and Chai assertion library. We will also use some additional assertions and utilities, especially for testing the events, by using truffle-assertions package.

We will start by creating an empty JavaScript file registration.js inside the test directory.

  • Install the truffle-assertion library and include this, along with contract artifacts, in the registration.js test file:
$ npm i truffle-assertions --save-dev

and inside the test file, include these line:

const Registration = artifacts.require('Registration')
const truffleAssert = require('truffle-assertions')
  • Create basic layout of the test file as given below:
contract('Registration', (accounts) => {})
  • We will set up some test variable and create a global instance of our smart contract before executing the tests:
  • Next, we will test the following functionality of the contract:
  • registerUser function should be executed only by the owner and it should emit the NewUser event:
it('should register a new user', async () => {
let result = await contract.registerUser(user, name, age, {
from: owner,
})
// test event
truffleAssert.eventEmitted(result, 'NewUser', (ev) => {
return ev.userAddress == user && ev.name == name && ev.age == age
})
})
  • The registerUser function should fail if caller is not the owner:
it('should fail to register by non-owner', async () => {
await truffleAssert.reverts(
contract.registerUser(user, name, age, {
from: notOwner,
}),
)
})
  • A registered user should be able to call the send function:
  • An un-registered user should not allow to call the send function:
it('should fail to send value from un-registered user', async () => {
await truffleAssert.reverts(
contract.send(value, {
from: unregistered,
}),
)
})

Writing Migrations

Create a new file 2_deploy_contracts.js inside the migrations directory and add the following contents to it:

const Registration = artifacts.require('Registration')module.exports = function (deployer) {
deployer.deploy(Registration)
}

The above migration is short and clean. However, we will look another way to export the contract ADDRESS and ABI during the migration so that we can use this metadata later to create a contract instance. This is useful especially in the case while calling the contract methods.

Add the following code block to the migrations file and it will create a Metadata.js file in the src directory:

Config Development Network

  • Uncomment following lines inside the truffle-config.js file to set up a local development network:
// development: {
// host: "127.0.0.1", // Localhost (default: none)
// port: 8545, // Standard Ethereum port (default: none)
// network_id: "*", // Any network (default: none)
// },
  • We need to install and run ganache-cli before executing the tests. Run the following command to install the ganache-cli as a development dependency:
$ npm i ganache-cli --save-dev

and then add the following scripts to the package.json file to run the ganache and test through npm:

"ganache": "ganache-cli -m 'electric gift gold bitter boring surface stumble swift peanut adult horse dish'",
"test": "truffle test"
  • Now open two terminals inside the app directory and then first run the ganache-cli:
$ npm run ganache

In the second terminal, run the test file:

$ npm run test

The result of the above command should look like this:

Solidity Code Coverage

Install the Solidity code coverage:

$ npm install --save-dev solidity-coverage

and add this to the plugin array of truffle-config.js file:

module.exports = {
networks: {...},
plugins: ["solidity-coverage"]
}

Add the following script to scripts in the package.json file:

"coverage": "truffle run coverage"

Finally, run the command:

$ npm run coverage

The output will contain the following code coverage metrics:

Eth Gas Reporter

Install the Eth gas reporter by running the following command:

$ npm install --save-dev eth-gas-reporter

config the reporter in the truffle-config.js file as:

mocha: {
// timeout: 100000
reporter: 'eth-gas-reporter',
reporterOptions: {
excludeContracts: ['Migrations'],
},
},

Now again run the npm run test command and the output will be like this:

At this point, your project may look like to this GitHub commit.

Backend Development

The back-end of the application is developed as a Node.js application. The application is developed using the JavaScript class and ES6syntax. The following snippet shows the node.js modules developed for the back-end.

Deploy Contract

Run the following command inside the app directory:

truffle migrate --reset --compile-all

The above command will generate the Metadata.js file in the src directory. The contains contract ADDRESS and ABI.

Modules

Let’s create another directory src inside the app directory that will serve as a container for our modules. The backend will contain the following modules:

  • Provider: A web3 provider is required to set up a connection with an Ethereum node, create the instance of the contract, and to call the methods from the smart contract. For that, install web3 as project dependency by running the following command:
$ npm install web3

Create a new file Provider.js and add the following code block to that:

  • Contract.js: This module will return the instance of the smart contract. Create and add the following block of code to Contract.js file:
  • Methods: This module will contain the functions to call the methods from the smart contract. For the demo purpose, we will call the sendmethod at a 30 seconds time interval. The contents of Methods.js will look like the following:
  • index.js: Finally, call the contract methods from the index.js file:
const Registration = require('./app/src/Methods')
const registration = new Registration()
registration.registerUser()
registration.sendDataInterval()

Let’s open a terminal inside the root directory and run the following command:

npm run app

As a result, you should see the transaction hash in the console.

The complete code can be found here.

Webpack Integration

A webpack is a static modular bundler which treats all files and assets as modules and relies on a dependency graph. Using the dependency graph, webpack statistically traverses all modules and their dependencies to build the graph and finally generate a single JavaScipt file that contains code from all modules in the correct order.

Webpack Main Points

To integrate webpack into the project, let’s have a brief look at the main points of the webpack:

  • Entry: This is the module that webpack uses to start building its internal dependency graph. From this module, webpack determines all other modules and libraries for the entry module and includes them in the graph until no dependency is left. For the current project, the entry point is index.js file
  • Output: This property defines a location where webpack will generate the bundle and a name for the bundle file.
  • Loaders: This is the property that webpack uses to transform the source code of non-JavaScript modules before they are added to the dependency graph.
  • Plugins: Plugins are helpful to do the tasks that loaders can’t do such as asset management, bundle minimization, and optimization, etc.
  • Mode: Mode property specifies built-in optimizations corresponding to each environment such as production, development, or none.

Webpack Configurations

Webpack uses a special configuration file, webpack.config.js, that specifies how assets and files will be transformed to generate the specified output. From the configurations, the webpack starts from the entry point and resolves each module it encounters while building the dependency graph. The final output will be a small bundle of all the modules of the project.

First, we need to install webpack and webpack-cli as project dependency. Run the following command inside the root directory of the project:

$ npm install webpack webpack-cli --save-dev

In the package.json file, we can create webpack tasks in the scripts section as follows:

"dev": "webpack --mode development",
"build": "webpack --mode production"

In the above scripts, we have used --mode flag to create development and production builds. Finally, add the following minimum configurations to the webpack.config.js file:

Run the $ npm run dev command inside the root directory and notices dist directory as a result. Change the directory to dist and run the bundle using the following command:

$ node bundle.js

Make sure you have ganache running and the artifacts are updated. If you could see the same output as before (transaction hashes), congratulations! you are all done🎉🎉🎉

Let’s tweak some more configurations to make the development experience a breeze.

  • Add watch flag to enable hot reload every time a file change. Change the dev script from
"dev": "webpack --mode development",

to

"dev": "webpack --mode development --w",
  • Clean the dist folder before each build. For this, we need to install and configure the clean-webpack-plugin by using the following command:
$ npm install --save-dev clean-webpack-plugin

Update the webpack.config.js by adding the following lines:

const { CleanWebpackPlugin } = require('clean-webpack-plugin');// other config, removed for brevityplugins: [
new CleanWebpackPlugin()
],

The complete project code can be found here.

Happy coding🎉🎉🎉

--

--