How to Build Your First DApp on Solana: A Step-by-Step Developer Guide

How to Build Your First DApp on Solana: A Step-by-Step Developer Guide

If you’re in the developer world, you’ve heard the buzz around Solana. Its promise of lightning-fast transactions and incredibly low fees has attracted massive attention. But for a developer, the real excitement isn’t just using the network; it’s building on it. So, where do you start? How do you go from being a traditional developer to building a decentralized application (DApp) on this powerful blockchain?

Don’t worry, it’s not as daunting as it sounds. This guide is designed to be your friendly co-pilot. We’re going to walk through the entire process together, from setting up your environment to deploying a live, interactive application. We won’t just copy-paste code; we’ll understand why we’re doing what we’re doing.

What We’ll Build Today: A simple but powerful “On-Chain Counter.” It will be a web page with a number displayed and a button. Every time a user clicks the button, they will sign a transaction to increment a counter whose state is stored directly on the Solana blockchain. This simple DApp covers all the core concepts you need to know.

Ready? Let’s get building.

 

Core Concepts: The Solana Mindset

Before we write a single line of code, let’s understand a few key differences in the Solana world.

  • What’s a Solana Program (aka Smart Contract)? On Solana, the backend logic that lives on the blockchain is called a “program.” These are typically written in Rust or C/C++ and compiled into a special format (BPF). A key thing to remember: Solana programs are stateless. They don’t store data themselves. They hold the logic, and they operate on separate data “accounts.”
  • The Anatomy of a DApp A DApp has two main parts:
    1. On-Chain Program: The Rust code we’ll deploy to the blockchain.
    2. Off-Chain Client: A web (or mobile) application that users interact with. This client communicates with the on-chain program.
  • The All-Important Account Model This is the most critical concept: on Solana, everything is an account.
    • Your wallet? It’s an account.
    • The program you deploy? It’s an account (marked as “executable”).
    • The data your program uses (like our counter)? It’s stored in a separate account. Your on-chain program will need to be told which accounts it’s allowed to read from or write to for any given instruction.

 

Part 1: Setting Up Your Developer Rig

Let’s get your machine ready for Solana development.

  1. Install Rust & Solana CLI: The Solana team provides a one-line command to install everything you need. Open your terminal and run:
    sh -c "$(curl -sSfL https://release.solana.com/v1.18.4/install)" 

    (Check the official docs for the latest version). After installation, close and reopen your terminal.

  2. Install Node.js: We’ll need Node.js for our frontend. If you don’t have it, download it from the official Node.js website.
  3. Install the Anchor Framework: Anchor is a framework that makes Solana development infinitely easier by handling a lot of the boilerplate for you. We’ll install it using avm (Anchor Version Manager).
    # Install avm cargo install --git https://github.com/coral-xyz/anchor avm --locked --force # Install the latest version of Anchor and set it as default avm install latest avm use latest 

Part 2: Building the On-Chain Program (The Backend)

This is where the magic begins. We’ll use Anchor to create our Rust program.

Initializing Your Anchor Project

Find a directory for your projects and run:

anchor init solana_counter cd solana_counter 

Anchor creates a bunch of files for you. The most important one for now is programs/solana_counter/src/lib.rs. This is where our Rust code lives.

Writing the Counter Logic in lib.rs

Open lib.rs and replace the contents with the following code. We’ll break it down right after.

use anchor_lang::prelude::*; declare_id!("YOUR_PROGRAM_ID"); // This will be replaced by Anchor #[program] pub mod solana_counter { use super::*; // This function runs once to create our counter account pub fn initialize(ctx: Context<Initialize>) -> Result<()> { let counter_account = &mut ctx.accounts.counter_account; counter_account.count = 0; msg!("Counter account created! Current count: 0"); Ok(()) } // This function increments the count pub fn increment(ctx: Context<Increment>) -> Result<()> { let counter_account = &mut ctx.accounts.counter_account; counter_account.count += 1; msg!("Counter incremented! New count: {}", counter_account.count); Ok(()) } } // 1. Validation struct for the 'initialize' function #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = user, space = 8 + 8)] pub counter_account: Account<'info, Counter>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } // 2. Validation struct for the 'increment' function #[derive(Accounts)] pub struct Increment<'info> { #[account(mut)] pub counter_account: Account<'info, Counter>, } // 3. The data structure for our counter account #[account] pub struct Counter { pub count: u64, // u64 is an 8-byte unsigned integer } 

Code Breakdown:

  • #[program]: This marks the main module containing our program’s instructions (initialize and increment).
  • initialize(ctx: Context<Initialize>): This is our setup function. It uses the context provided by Initialize to create a new account for our counter and set its initial count to 0.
  • increment(ctx: Context<Increment>): This function takes the existing counter account, accesses its data, and increases the count by 1.
  • #[derive(Accounts)]: These structs are the heart of Anchor’s security. They define which accounts are required for an instruction and what constraints apply to them.
    • In Initialize, we say: “We need to init (create) a new counter_account, the user (who must be a Signer) will pay for it, and we need the System program to handle the account creation.” The space argument reserves 8 bytes for a standard header plus 8 bytes for our u64 count.
    • In Increment, we simply say: “We need mutable (mut) access to an existing counter_account.”
  • #[account]: This defines the structure of the data we’re storing on-chain.

 

Building and Testing Your Program

Now, let’s make sure it works. Anchor makes this incredibly simple.

# Build the program anchor build # Run the tests (Anchor creates a test file for you) anchor test 

The default test will fail because we changed the program. But this confirms our code compiles!

Part 3: Deploying to the Blockchain

Let’s put our code on a live (but not production) network.

Connecting to Devnet

We’ll use Solana’s Devnet, a test network for developers.

solana config set --url devnet

Getting Free SOL (Airdrop)

We need a tiny bit of SOL to pay for the deployment transaction fees.

solana airdrop 2 

This will send 2 SOL to your default wallet.

Running the Deploy Command

This one command handles everything:

anchor deploy 

After a minute, you’ll get a “Program Deployed” message with your Program ID. Anchor automatically places this ID in declare_id! and in your project’s configuration file.

Part 4: Building the Frontend Interface (The Frontend)

Now, let’s create a web interface so users can interact with our on-chain program.

Setting Up a React App

We’ll use Next.js, but any React framework will do. Navigate to your project’s root directory (solana_counter) and run:

npx create-next-app@latest tests/app 

We place it in tests/app to keep things organized.

 

Connecting to a Solana Wallet

First, we need to install the necessary libraries to talk to wallets like Phantom or Solflare and to our program. Navigate into the new app directory (cd tests/app) and run:

npm install @solana/web3.js @coral-xyz/anchor @solana/wallet-adapter-react @solana/wallet-adapter-react-ui @solana/wallet-adapter-wallets 

Next, open pages/_app.js and wrap your application with the wallet providers. This is a standard setup for any Solana DApp.

JavaScript

// pages/_app.js import React from 'react'; import { WalletAdapterNetwork } from '@solana/wallet-adapter-base'; import { ConnectionProvider, WalletProvider } from '@solana/wallet-adapter-react'; import { WalletModalProvider } from '@solana/wallet-adapter-react-ui'; import { PhantomWalletAdapter } from '@solana/wallet-adapter-wallets'; import { clusterApiUrl } from '@solana/web3.js'; require('@solana/wallet-adapter-react-ui/styles.css'); import '../styles/globals.css'; function MyApp({ Component, pageProps }) { const network = WalletAdapterNetwork.Devnet; const endpoint = React.useMemo(() => clusterApiUrl(network), [network]); const wallets = React.useMemo(() => [new PhantomWalletAdapter()], []); return ( <ConnectionProvider endpoint={endpoint}> <WalletProvider wallets={wallets} autoConnect> <WalletModalProvider> <Component {...pageProps} /> </WalletModalProvider> </WalletProvider> </ConnectionProvider> ); } export default MyApp;

Calling Your On-Chain Program from the UI

Now for the main logic. Replace the content of pages/index.js with this:

JavaScript

// pages/index.js import { useState, useEffect } from 'react'; import { Connection, PublicKey } from '@solana/web3.js'; import { Program, AnchorProvider, web3 } from '@coral-xyz/anchor'; import { useWallet } from '@solana/wallet-adapter-react'; import { WalletMultiButton } from '@solana/wallet-adapter-react-ui'; import idl from '../idl.json'; // The interface definition for our program // Get the program ID from the Anchor configuration const programID = new PublicKey(idl.metadata.address); export default function Home() { const [count, setCount] = useState(null); const [counterAccount, setCounterAccount] = useState(null); const wallet = useWallet(); async function getProvider() { if (!wallet.connected) { return null; } const network = "https://api.devnet.solana.com"; const connection = new Connection(network, "processed"); const provider = new AnchorProvider(connection, wallet, { preflightCommitment: "processed" }); return provider; } async function initializeCounter() { const provider = await getProvider(); if (!provider) { alert("Wallet not connected!"); return; } const program = new Program(idl, programID, provider); const newAccount = web3.Keypair.generate(); try { await program.methods .initialize() .accounts({ counterAccount: newAccount.publicKey, user: provider.wallet.publicKey, systemProgram: web3.SystemProgram.programId, }) .signers([newAccount]) .rpc(); setCounterAccount(newAccount.publicKey); await fetchCount(newAccount.publicKey); } catch (err) { console.error("Error initializing counter:", err); } } async function incrementCount() { const provider = await getProvider(); if (!provider || !counterAccount) { alert("Initialize the counter first!"); return; } const program = new Program(idl, programID, provider); try { await program.methods .increment() .accounts({ counterAccount: counterAccount, }) .rpc(); await fetchCount(counterAccount); } catch (err) { console.error("Error incrementing counter:", err); } } async function fetchCount(accountPk) { const provider = await getProvider(); if (!provider || !accountPk) return; const program = new Program(idl, programID, provider); const account = await program.account.counter.fetch(accountPk); setCount(account.count.toString()); } // You would typically store the counterAccount.publicKey in local storage // so you don't have to initialize every time. For this guide, we keep it simple. return ( <div style={{ padding: '20px', display: 'flex', flexDirection: 'column', alignItems: 'center' }}> <h1>Solana Counter DApp</h1> <WalletMultiButton /> <div style={{ marginTop: '20px' }}> <button onClick={initializeCounter} style={{ marginRight: '10px' }}>Initialize Counter</button> <button onClick={incrementCount}>Increment Counter</button> </div> <h2 style={{ marginTop: '30px' }}> Count: {count !== null ? count : "Not initialized"} </h2> </div> ); } 

Important: You need to copy the IDL file generated by Anchor into your app’s directory. Run this command from the project root (solana_counter): cp target/idl/solana_counter.json tests/app/idl.json

Now, navigate to tests/app and run npm run dev. Open your browser to http://localhost:3000. You should see your DApp! Connect your wallet, click “Initialize Counter,” and then “Increment Counter” to see the magic happen on-chain.

Conclusion: You’re a Solana Developer Now!

Congratulations! You have successfully built and deployed a full-stack decentralized application on Solana. You’ve learned how to:

  • Set up a professional development environment.
  • Write and deploy a stateless on-chain Rust program using Anchor.
  • Build a web frontend that connects to a user’s wallet.
  • Call your on-chain program’s instructions from a web app.

This is a massive first step. From here, you can explore more complex DApps, interact with other protocols, or dive deeper into the Solana Program Library (SPL). The ecosystem is vast, and you now have the foundational skills to explore it.

Welcome to the world of Solana development!

2 Comments

  1. Marinade

    The perfect guide for beginners!

  2. Yutaro Mori

    Today I woke up to become a developer!

Leave a Comment