InsightsBlockchain

Build a Full-Stack dApp in 2026 (Next.js, wagmi, viem)

How to build a dApp in 2026: connect wallets, read and write a Solidity contract, show events and deploy, using Next.js, wagmi v3 and viem. Testnet code.

Digital Silk Route8 min read

Build a Full-Stack dApp in 2026 (Next.js, wagmi, viem)

To build a dApp in 2026, deploy a tested smart contract to a testnet, then create a Next.js app that uses wagmi for wallet connection and React hooks, viem for low-level Ethereum calls, and TanStack Query for caching. The frontend reads contract state, sends transactions from the user's wallet, waits for confirmation and shows on-chain events. This guide builds a complete tipping dApp on Sepolia using wagmi v3, which renamed some hooks you will see in older tutorials.

Key takeaways

  • A dApp has three layers: the contract (on-chain), the frontend (in the browser), and data access (RPC calls and, at scale, an indexer).
  • wagmi v3 renamed useAccount to useConnection and mutation hooks now return mutate and mutateAsync.
  • Connector SDKs (MetaMask, WalletConnect, Coinbase) are optional peer dependencies in wagmi v3; the injected connector needs none.
  • Always handle loading, error, wrong-network and "waiting for confirmation" states.
  • Keep everything on testnet until the contract is reviewed.

dApp architecture in one picture

Browser (Next.js + wagmi)
   │  read: eth_call via RPC          write: user signs in wallet
   ▼                                   ▼
RPC provider ───────────────► Ethereum / L2 network ◄── TipJar contract
   ▲
   │  events (getLogs) or an indexer for history and search
  • Reads (view functions) are free calls through an RPC provider.
  • Writes are transactions the user signs in their wallet and pays gas for.
  • Events are logs the contract emits; you query them for history.

We use the TipJar contract from BC-02. If you have not deployed it yet, follow that tutorial first and note the contract address and the block number it was deployed in.

Step 1: Create the Next.js project

npx create-next-app@latest tipjar-dapp --typescript --app
cd tipjar-dapp
npm install wagmi viem @tanstack/react-query

wagmi v3 requires TypeScript 5.9.3 or newer, which current create-next-app templates already use.

Add your RPC URL to .env.local (this is a public endpoint URL, not a secret key, but keep paid keys restricted by domain in your provider's dashboard):

NEXT_PUBLIC_SEPOLIA_RPC_URL=https://your-sepolia-rpc-url
NEXT_PUBLIC_TIPJAR_ADDRESS=0xYourDeployedTipJarAddress
NEXT_PUBLIC_TIPJAR_DEPLOY_BLOCK=0

Set NEXT_PUBLIC_TIPJAR_DEPLOY_BLOCK to the block number shown for your deployment on Sepolia Etherscan.

Step 2: Configure wagmi

Create lib/wagmi.ts:

import { http, createConfig } from 'wagmi'
import { sepolia } from 'wagmi/chains'
import { injected } from 'wagmi/connectors'

export const config = createConfig({
  chains: [sepolia],
  connectors: [injected()],
  transports: {
    [sepolia.id]: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL),
  },
  ssr: true,
})

declare module 'wagmi' {
  interface Register {
    config: typeof config
  }
}
  • injected() works with browser-extension wallets such as MetaMask without extra packages.
  • ssr: true avoids hydration mismatches in Next.js.
  • The Register block gives you full type inference across hooks.

If you later want WalletConnect for mobile wallets, install @walletconnect/ethereum-provider and add the walletConnect({ projectId }) connector. In wagmi v3 these SDKs are no longer bundled.

Step 3: Add the providers

Create app/providers.tsx:

'use client'

import { useState, type ReactNode } from 'react'
import { WagmiProvider } from 'wagmi'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { config } from '@/lib/wagmi'

export function Providers({ children }: { children: ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())
  return (
    <WagmiProvider config={config}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WagmiProvider>
  )
}

Wrap the app in app/layout.tsx:

import type { ReactNode } from 'react'
import { Providers } from './providers'

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

Step 4: Add the contract ABI

The ABI tells viem how to encode calls. You can copy it from out/TipJar.sol/TipJar.json in your Foundry project. For type safety, declare it as const.

Create lib/tipjar.ts:

export const TIPJAR_ADDRESS = process.env.NEXT_PUBLIC_TIPJAR_ADDRESS as `0x${string}`

export const tipJarAbi = [
  {
    type: 'function',
    name: 'tip',
    stateMutability: 'payable',
    inputs: [{ name: 'message', type: 'string' }],
    outputs: [],
  },
  {
    type: 'function',
    name: 'totalTips',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ name: '', type: 'uint256' }],
  },
  {
    type: 'function',
    name: 'owner',
    stateMutability: 'view',
    inputs: [],
    outputs: [{ name: '', type: 'address' }],
  },
  {
    type: 'event',
    name: 'Tipped',
    inputs: [
      { name: 'from', type: 'address', indexed: true },
      { name: 'amount', type: 'uint256', indexed: false },
      { name: 'message', type: 'string', indexed: false },
    ],
  },
] as const

Step 5: Connect a wallet

Create components/ConnectButton.tsx:

'use client'

import { useConnection, useConnect, useConnectors, useDisconnect } from 'wagmi'

export function ConnectButton() {
  const { address, isConnected, chain } = useConnection()
  const connectors = useConnectors()
  const { mutate: connect, isPending } = useConnect()
  const { mutate: disconnect } = useDisconnect()

  if (isConnected) {
    return (
      <div>
        <p>Connected: {address} {chain ? `on ${chain.name}` : '(unsupported network)'}</p>
        <button onClick={() => disconnect()}>Disconnect</button>
      </div>
    )
  }

  return (
    <div>
      {connectors.map((connector) => (
        <button
          key={connector.uid}
          disabled={isPending}
          onClick={() => connect({ connector })}
        >
          Connect {connector.name}
        </button>
      ))}
    </div>
  )
}

chain is undefined when the wallet is on a network that is not in your config. That is your cue to show a "Switch to Sepolia" button using useSwitchChain.

Step 6: Read and write the contract

Create components/TipForm.tsx:

'use client'

import { useEffect, useState, type FormEvent } from 'react'
import { formatEther, parseEther } from 'viem'
import {
  useConnection,
  useReadContract,
  useWaitForTransactionReceipt,
  useWriteContract,
} from 'wagmi'
import { TIPJAR_ADDRESS, tipJarAbi } from '@/lib/tipjar'

export function TipForm() {
  const { isConnected } = useConnection()
  const [amount, setAmount] = useState('0.001')
  const [message, setMessage] = useState('')

  const { data: total, refetch } = useReadContract({
    address: TIPJAR_ADDRESS,
    abi: tipJarAbi,
    functionName: 'totalTips',
  })

  const { data: hash, mutate: writeContract, isPending, error } = useWriteContract()
  const { isLoading: isConfirming, isSuccess } = useWaitForTransactionReceipt({ hash })

  useEffect(() => {
    if (isSuccess) refetch()
  }, [isSuccess, refetch])

  function submit(e: FormEvent) {
    e.preventDefault()
    writeContract({
      address: TIPJAR_ADDRESS,
      abi: tipJarAbi,
      functionName: 'tip',
      args: [message],
      value: parseEther(amount),
    })
  }

  return (
    <form onSubmit={submit}>
      <p>Total tipped: {total !== undefined ? formatEther(total) : '...'} ETH (testnet)</p>
      <input value={amount} onChange={(e) => setAmount(e.target.value)} placeholder="0.001" />
      <input value={message} onChange={(e) => setMessage(e.target.value)} placeholder="Message" />
      <button type="submit" disabled={!isConnected || isPending || isConfirming}>
        {isPending ? 'Confirm in wallet...' : isConfirming ? 'Waiting for block...' : 'Send tip'}
      </button>
      {hash && <p>Transaction: {hash}</p>}
      {isSuccess && <p>Confirmed.</p>}
      {error && <p>Error: {error.message}</p>}
    </form>
  )
}

What is happening:

  • useReadContract performs an eth_call and caches the result with TanStack Query.
  • useWriteContract asks the wallet to sign and send. In wagmi v3 the function is mutate, renamed here to writeContract for readability.
  • useWaitForTransactionReceipt waits until the transaction is mined, then we refetch the total.
  • parseEther and formatEther from viem convert between ETH strings and bigint wei.

Step 7: Show recent tips from events

For a small app, read events directly with viem. Create lib/getTips.ts:

import { createPublicClient, http, parseAbiItem } from 'viem'
import { sepolia } from 'viem/chains'
import { TIPJAR_ADDRESS } from './tipjar'

const client = createPublicClient({
  chain: sepolia,
  transport: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL),
})

export async function getRecentTips() {
  const logs = await client.getLogs({
    address: TIPJAR_ADDRESS,
    event: parseAbiItem('event Tipped(address indexed from, uint256 amount, string message)'),
    fromBlock: BigInt(process.env.NEXT_PUBLIC_TIPJAR_DEPLOY_BLOCK ?? '0'),
    toBlock: 'latest',
  })
  return logs.map((log) => ({
    from: log.args.from,
    amount: log.args.amount,
    message: log.args.message,
    txHash: log.transactionHash,
  }))
}

You can call this from a server component or a route handler and render the list. For live updates in the browser, wagmi's useWatchContractEvent hook subscribes to new events.

When you need an indexer

Many RPC providers limit how many blocks one getLogs call can scan. Once your contract has a long history, or you need search, filtering and pagination, use an indexer such as The Graph or Ponder. It listens to events, stores them in a database and gives your frontend a fast API. That is also a common freelance task (see BC-08).

Step 8: Put it on the page

Edit app/page.tsx:

import { ConnectButton } from '@/components/ConnectButton'
import { TipForm } from '@/components/TipForm'

export default function Home() {
  return (
    <main>
      <h1>TipJar (Sepolia testnet)</h1>
      <ConnectButton />
      <TipForm />
    </main>
  )
}

Run it:

npm run dev

Open http://localhost:3000, connect a wallet set to Sepolia with test ETH, and send a tip.

Step 9: Deploy the frontend

  • Push the project to GitHub.
  • Deploy on Vercel, Netlify or any host that supports Next.js, and add the same NEXT_PUBLIC_ variables in the host's settings.
  • Restrict your RPC key to your domain in the provider's dashboard so others cannot reuse it.
  • Link the deployed site in your contract's README and in your portfolio.

Production checklist for a dApp

AreaWhat to check
NetworkDetect wrong chain; offer a switch button
UX statesWallet prompt, pending, confirming, success, error
ErrorsShow human messages for user rejection and reverts (custom errors decode with the ABI)
AmountsValidate input before parseEther; never use floating point for money
SecurityNo private keys in frontend code; contract reviewed (BC-03)
DataIndexer for history once logs grow
MobileWalletConnect or a mobile-friendly connector
DisclosuresClear "testnet" labels and terms if real users are involved

Turning the tutorial into a product

This dApp is a template that many people need in some form: donation pages for creators, payment links, event check-ins, membership passes. A clean, documented starter kit with tests and a deploy guide is a digital product. A custom version for a client is a service. Either way, ship it on testnet first and be honest about what it does and does not do.

Next step with Digital Silk Route

A full-stack dApp is a natural capstone in the Blockchain track of the Digital Silk Route (DSR) AI & Crypto Academy, which runs from Beginner to Intermediate to Advanced with a shippable project at every level. Once your dApp or starter kit is tested and documented, you can list it on the DSR Marketplace for other developers and businesses. Learn. Build. Earn.

Sources

This article is for education only and is not financial, legal or investment advice.

Frequently asked questions

  • how to build a dapp
  • wagmi tutorial
  • viem tutorial
  • nextjs web3
  • connect wallet react
  • full stack dapp