Best Ethereum Web Development Stack in 2024
Tags: ethereum, javascript, webdev, programming
Published: 2024-04-02 13:31:47

In this manual, I will cover the most effective tools for developing decentralised applications on the Ethereum blockchain. I built numerous projects with this stack, so I am confident that they are optimal for current development practices. 

## UI

### Foundation: Next.js

We'll use the open-source web development framework Next.js as the foundation of our UI. That's a better option than pure React because it includes a lot of things out of the box, such as optimizations, polyfills, integration examples, and much more. This will save us a lot of time so we can focus on actual development.

### EVM Utilities: viem

To interact with Ethereum, we will use [viem](https://viem.sh/), a modern fully type-safe TypeScript interface that provides low-level stateless primitives for blockchain interactions. It contains everything we'll need, from transaction and log utilities to functions for ENS resolving and much more.

#### Key Features

Here is a list of Viem's major advantages:

- It is highly [performant](https://viem.sh/docs/introduction#performance) and much smaller in [size](https://viem.sh/docs/introduction#bundle-size) than alternatives, which is an important consideration for front-end app bundles.
- It can [automatically infer](https://viem.sh/docs/typescript) types from the provided [ABI](https://docs.soliditylang.org/en/latest/abi-spec.html) without requiring any additional steps or packages, such as [typechain](https://github.com/dethcrypto/TypeChain) for [ethers](https://docs.ethers.org/). Just don't forget to export ABIs `as const`!
- It is fully modular and easily extensible. You get two client primitives: public (for public actions) and wallet (for signing actions), which can be extended or customised to your liking.
- Viem supports contract wallet signature verification ([ERC-1271](https://eips.ethereum.org/EIPS/eip-1271)) as well as regular EOA signatures via [`publicClient.verifyMessage()`](https://viem.sh/docs/actions/public/verifyMessage), allowing you to prepare for Account Abstraction mainstream adoption.
- On RPC errors, viem logs [detailed explanations](https://x.com/_jxom/status/1633240447761121281) of what happened rather than just throwing a generic error message or the error ID, saving you a lot of debugging time.

#### Extensions

There is also excellent tooling to extend viem.

For example, [Pimlico](https://www.pimlico.io/)'s [permissionless](https://docs.pimlico.io/permissionless) offers clients additional functions for interacting with the [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) Account Abstraction infrastructure. It allows you to deploy smart accounts, create [User Operations](https://www.biconomy.io/post/understanding-user-operations-a-deep-dive-into-flexible-transaction-models), send them to a bundler, and sponsor gas with a paymaster.

```typescript
// Import the required modules.
import { createBundlerClient } from "permissionless"
import { sepolia } from "viem/chains"
import { http } from "viem"
 
// Create the required clients.
const bundlerClient = createBundlerClient({
    chain: sepolia,
    transport: http(bundlerUrl), // Use any bundler url
    entryPoint: "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789",
})
 
// Consume bundler, paymaster, and smart account actions!
const opReceipt = await bundlerClient.getUserOperationReceipt({
    hash: userOpHash
})
 
// Build with strict TypeScript types
opReceipt!.actualGasUsed // actualGasUsed: bigint
```

One more example is [reversemirage](https://www.reversemirage.com/), which provides clients with a set of functions for the most common token standards, such as ERC-20. For example, for [ERC-20](https://ethereum.org/developers/docs/standards/tokens/erc-20), you would get helpers such as `getERC20BalanceOf` for public clients and `writeERC20Transfer` for wallet clients, eliminating the need to manually import and pass the ABI.

```typescript
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
import { publicActionsReverseMirage, amountToNumber } from 'reverse-mirage'

export const publicClient = createPublicClient({
  chain: mainnet,
  transport: http()
}).extend(publicActionsReverseMirage)

// read token metadata
const usdc = await publicClient.getERC20({
  address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // usdc
  id: mainnet.id,
})

console.log(usdc.decimals) // 6
console.log(usdc.name) // USD Coin

// read a balance
const vitalikBalance = await publicClient.getERC20Balance({
  erc20: usdc,
  address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' // vitalik
})

console.log(vitalikBalance.amount) // 420690000n
console.log(amountToNumber(vitalikBalance)) // 420.69
```

Viem is also starting to support L2s such as [Optimism](https://viem.sh/op-stack) and [zkSync](https://viem.sh/zksync) through extensions.

```typescript
import { createPublicClient, http, parseEther } from 'viem'
import { mainnet } from 'viem/chains'
 
const client = createPublicClient({
  chain: mainnet,
  transport: http(),
}).extend(publicActionsL2()) 
 
const l1Gas = await client.estimateL1Gas({ 
  account: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', 
  to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8', 
  value: parseEther('1') 
}) 
```

### React Hooks: wagmi

We are going with [wagmi](https://wagmi.sh/) for hooks, and I'll explain why below.

```typescript
import { WagmiProvider } from 'wagmi'
import { http, createConfig } from 'wagmi'
import { mainnet } from 'wagmi/chains'

export const config = createConfig({
  chains: [mainnet],
  transports: {
    [mainnet.id]: http(),
  },
})

function App() {
  return (
    <WagmiProvider config={config}> 
      {/** ... */} 
    </WagmiProvider> 
  )
}
```

First and foremost, it ensures full type safety for hooks through ABI and [EIP-712](https://eips.ethereum.org/EIPS/eip-712) Typed Data type inference.

Wagmi is built on Viem, so we're not adding any major dependencies to our project other than [TanStack Query](https://tanstack.com/query/latest).

Wagmi benefits from TanStack Query's caching and state deduplication. By default, cache is in memory, but persistent storage can be configured to [store state in localStorage](https://wagmi.sh/react/guides/tanstack-query#persistence-via-external-stores).

Wagmi supports auto state refresh on network and address changes, as well as built-in automatic [multicall](https://docs.uniswap.org/contracts/v3/reference/periphery/base/Multicall) support, which improves data fetching speed for contracts with multiple interactions.

It already fully supports [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963), which is a significant improvement to the wallet connection experience by allowing wallets to advertise themselves to wagmi rather than gambling for access to `window.ethereum`.

One of Wagmi's lesser-known features is its [CLI](https://wagmi.sh/cli/why), which enables custom hook generation from provided ABIs. You can pre-generate hooks for your contracts, eliminating the need to import and pass ABIs and contract addresses, which is quite useful.

### Data Fetching Hooks: TanStack Query

Because wagmi 2.0 added [TanStack Query](https://tanstack.com/query/latest) as a peer dependency, we'll use it for any other data fetching, such as data from our indexer or third-party API services.

Although [SWR](https://swr.vercel.app/) is a good alternative, there are no significant features or API differences to justify using both data fetching libraries in the same app.

## Backend for Contract Indexing: Ponder

### Intro

Often, fetching contract data on each page load is inefficient or we must compute aggregated data. We'll need a separate indexing backend that will track the chain head and automatically update the database.

We are using [Ponder](https://ponder.sh/) for this.

Ponder, like most indexers, provides a GraphQL API. For fetching data, I recommend creating a custom TanStack Query hook on the frontend and using [graphql-request](https://github.com/jasonkuhrt/graphql-request) as a fetcher due to its small footprint.

### Features

It is based on viem and [ABIType](https://github.com/wagmi-dev/abitype), so we'll have the same familiar API for accessing event data and making on-chain requests, complete with excellent type safety and autocomplete.

One of Ponder's standout features is its excellent development server. It allows for hot reloading of all project files and provides detailed error logs.

Ponder runs in Node.js, so we can import NPM packages, make fetch requests while indexing, and even connect to external databases if we want to.

It is also multi-chain, allowing it to index and aggregate data in a single database across multiple chains at once.

### Speed

Ponder [claims](https://ponder.sh/blog/introducing-ponder) that it is also very fast: indexing an ERC-20 contract is approximately 10 times faster than the Subgraph Graph Node from a cold start and 15 times faster when fully cached.

Keep in mind, however, that [Subgraphs](https://thegraph.com/docs/en/quick-start/) are specifically designed for the [Decentralized Network](https://thegraph.com/docs/en/network/overview/), so speed and flexibility are intentionally sacrificed in favour of perfectly reproducible indexing.

### Deployment

In terms of infrastructure, Ponder uses SQLite for development, but it will require a PostgreSQL database to store data for production use.

Ponder offers one-click deployments on services such as [Railway](https://railway.app/), but it can also be hosted on your own server with Docker. It also supports zero-downtime deployments by performing a health check that returns "healthy" only after all events have been indexed. Built-in Prometheus metrics and detailed logs allow you to monitor indexing progress and identify indexing errors.

### Developer Experience

In contrast to [Subgraphs](https://thegraph.com/docs/en/quick-start/), you'll see a single auto-generated indexer instance rather than having to manually import each event and entity.

```typescript
import { ponder } from "@/generated";

ponder.on("Blitmap:Transfer", async ({ event, context }) => {
  const { BlitmapToken } = context.db;
  await BlitmapToken.create({
    id: event.args.tokenId,
    data: {
      owner: event.args.to,
    },
  });
});
```

One of the patterns I personally liked when working with Ponder is built-in upsert function for entities, where from one function call you can provide different logic for creating and updating an entity:

```typescript
  // Create an Account for the sender, or update the balance if it already exists.
  await Account.upsert({
    id: event.args.from,
    create: {
      balance: BigInt(0),
      isOwner: false,
    },
    update: ({ current }) => ({
      balance: current.balance - event.args.value,
    }),
  });
```

## Conclusion

In this article, I've used my practical experience and insights to provide a streamlined approach to developing decentralised applications on Ethereum. I've presented a robust and efficient framework that combines the best features of Next.js, Viem, Wagmi, and Ponder with advanced extensions and tools. Using this stack, developers can ensure that their DApps are not only scalable and secure, but also stay ahead of the rapidly evolving blockchain ecosystem.