Developer Quickstart

Build Your First Stacks App in 30 Minutes
Looking to see what building on Stacks is all about? You're in the right place.
This tutorial will help you build a working Stacks application in just 30 minutes. You'll learn the essential tools and concepts needed to build decentralized applications on Stacks, the leading Bitcoin L2.
What you'll build: A simple message board where users can post messages to the blockchain and read messages from others.
What you'll learn:
How to write a Clarity smart contract
How to deploy contracts to Stacks testnet
How to connect a wallet to your app
How to interact with contracts from a frontend
Prerequisites:
Basic familiarity with web development (HTML, CSS, JavaScript)
A modern web browser
30 minutes of your time
Let's get started!
Step 1: Set Up Your Wallet (5 minutes)
First, you'll need a Stacks wallet to interact with the blockchain.
Install Leather Wallet
Visit leather.io and install the browser extension
Create a new wallet or import an existing one
Important: Switch to the Testnet network in your wallet settings
Get testnet STX tokens from the Stacks Testnet Faucet
Your wallet is now ready for testnet development!
Step 2: Write Your First Clarity Contract (10 minutes)
Clarity is Stacks' smart contract language, designed for safety and predictability. Let's write a simple message board contract.
Clarity is inspired by LISP and uses a functional programming approach. Everything in Clarity is an expression wrapped in parentheses. This can be a bit overwhelming at first if you are used to languages like JavaScript or Solidity, but the learning curve is short and Clarity is a simple language to understand once you dive in and start using it.
For a more detailed introduction, check out the Clarity Crash Course in the docs.
Write the Contract
Open Clarity Playground in your browser. This is an online IDE where you can write and test Clarity code without installing anything.
Delete the existing code and replace it with this message board contract:
;; Simple Message Board Contract
;; This contract allows users to post and read messages
;; Define a map to store messages
;; Key: message ID (uint), Value: message content (string-utf8 280)
(define-map messages uint (string-utf8 280))
;; Define a map to store message authors
(define-map message-authors uint principal)
;; Counter for message IDs
(define-data-var message-count uint u0)
;; Public function to add a new message
(define-public (add-message (content (string-utf8 280)))
(let ((id (+ (var-get message-count) u1)))
(map-set messages id content)
(map-set message-authors id tx-sender)
(var-set message-count id)
(ok id)))
;; Read-only function to get a message by ID
(define-read-only (get-message (id uint))
(map-get? messages id))
;; Read-only function to get message author
(define-read-only (get-message-author (id uint))
(map-get? message-authors id))
;; Read-only function to get total message count
(define-read-only (get-message-count)
(var-get message-count))
;; Read-only function to get the last few messages
(define-read-only (get-recent-messages (count uint))
(let ((total-count (var-get message-count)))
(if (> count total-count)
(map get-message (list u1 u2 u3 u4 u5))
(map get-message (list
(- total-count (- count u1))
(- total-count (- count u2))
(- total-count (- count u3))
(- total-count (- count u4))
(- total-count (- count u5)))))))Test the Contract
Click "Deploy", and go to the command line in the bottom right corner and try calling the functions.
We are using the contract-call? method to call the functions in the contract that we just deployed within the playground.
;; Test adding a message
(contract-call? .contract-1 add-message u"Hello, Stacks!")
;; Test reading the message
(contract-call? .contract-1 get-message u1)
;; Test getting the count
(contract-call? .contract-1 get-message-count)You should see the contract working in the evaluation panel on the right!
Key Clarity Concepts Explained
define-map: creates a map / key-value store on-chain (like a simple table).define-data-var: creates a single persistent variable (used for counters, settings).define-public: public function that can modify blockchain state.define-read-only: functions that can only read state and don't modify it.tx-sender: automatically set to the address of whoever called the function (useful for authentication).let: create local variables inside functions.All public functions return a response type:
(ok value)or(err error).
Step 3: Deploy Your Contract (5 minutes)
Now let's deploy your contract to the Stacks testnet so you can interact with it from a web application.
Deploy via Stacks Explorer
Visit the Stacks Explorer Sandbox
Connect your Leather wallet (make sure you're on testnet)
Paste your contract code into the editor
Give your contract a name (e.g., "message-board") or just use the default generated name
Click "Deploy Contract"
Confirm the transaction in your wallet
The deployment should only take a few seconds. Once complete, you'll see your contract address in the explorer. Here's an example transaction deploying this contract.
Test Your Deployed Contract
In the explorer, find your deployed contract
Scroll down a bit and click on "Available Functions" to view its functions
Try calling
add-messagewith a test message (you'll need to change the post conditions toggle to allow mode, there is a dedicated docs page talking about Post Conditions on Stacks)Call
get-messagewith IDu1to read it backCall
get-message-countto see the total
Your contract is now live and functional on the blockchain!
Step 4: Build the Frontend (10 minutes)
Let's create a simple web interface to interact with your contract.
Set Up the Project
Create a new React project:
npm create vite@latest my-message-board -- --template react
cd my-message-board
npm installInstall the Stacks.js libraries:
npm install @stacks/connect @stacks/transactions @stacks/networkCreate the App Component
Replace the contents of src/App.jsx with the following:
import { useState, useEffect } from "react";
import { connect, disconnect, isConnected, request } from "@stacks/connect";
import {
fetchCallReadOnlyFunction,
stringUtf8CV,
uintCV,
} from "@stacks/transactions";
import "./App.css";
const network = "testnet";
// Replace with your contract address
const CONTRACT_ADDRESS = "YOUR_CONTRACT_ADDRESS_HERE";
const CONTRACT_NAME = "message-board";
function App() {
const [connected, setConnected] = useState(false);
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState("");
const [loading, setLoading] = useState(false);
useEffect(() => {
setConnected(isConnected());
if (isConnected()) {
loadMessages();
}
}, []);
// Check for connection changes
useEffect(() => {
const checkConnection = () => {
const connectionStatus = isConnected();
if (connectionStatus !== connected) {
setConnected(connectionStatus);
if (connectionStatus) {
loadMessages();
}
}
};
const intervalId = setInterval(checkConnection, 500);
return () => clearInterval(intervalId);
}, [connected]);
const connectWallet = async () => {
try {
await connect({
appDetails: {
name: "Message Board",
icon: window.location.origin + "/logo.svg",
},
onFinish: () => {
setConnected(true);
// Small delay to ensure connection is fully established
setTimeout(() => {
loadMessages();
}, 100);
},
});
} catch (error) {
console.error("Connection failed:", error);
}
};
const disconnectWallet = () => {
disconnect();
setConnected(false);
setMessages([]);
};
const loadMessages = async () => {
try {
// Get message count
const countResult = await fetchCallReadOnlyFunction({
contractAddress: CONTRACT_ADDRESS,
contractName: CONTRACT_NAME,
functionName: "get-message-count",
functionArgs: [],
network,
senderAddress: CONTRACT_ADDRESS,
});
const count = parseInt(countResult.value);
// Load recent messages
const messagePromises = [];
for (let i = Math.max(1, count - 4); i <= count; i++) {
messagePromises.push(
fetchCallReadOnlyFunction({
contractAddress: CONTRACT_ADDRESS,
contractName: CONTRACT_NAME,
functionName: "get-message",
functionArgs: [uintCV(i)],
network,
senderAddress: CONTRACT_ADDRESS,
})
);
}
const messageResults = await Promise.all(messagePromises);
const loadedMessages = messageResults
.map((result, index) => ({
id: count - messageResults.length + index + 1,
content: result.value.value,
}))
.filter((msg) => msg.content !== undefined);
setMessages(loadedMessages);
} catch (error) {
console.error("Error loading messages:", error);
}
};
const postMessage = async () => {
if (!newMessage.trim()) return;
setLoading(true);
try {
const result = await request("stx_callContract", {
contract: `${CONTRACT_ADDRESS}.${CONTRACT_NAME}`,
functionName: "add-message",
functionArgs: [stringUtf8CV(newMessage)],
network,
});
console.log("Transaction submitted:", result.txid);
setNewMessage("");
// Reload messages after a delay to allow the transaction to process
setTimeout(() => {
loadMessages();
setLoading(false);
}, 2000);
} catch (error) {
console.error("Error posting message:", error);
setLoading(false);
}
};
return (
<div className="App">
<header className="App-header">
<h1>📝 Stacks Message Board</h1>
{!connected ? (
<button onClick={connectWallet} className="connect-button">
Connect Wallet
</button>
) : (
<button onClick={disconnectWallet} className="disconnect-button">
Disconnect
</button>
)}
</header>
{connected && (
<main className="App-main">
<div className="post-message">
<h2>Post a Message</h2>
<div className="message-input">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="What's on your mind?"
maxLength={280}
disabled={loading}
/>
<button
onClick={postMessage}
disabled={loading || !newMessage.trim()}
>
{loading ? "Posting..." : "Post"}
</button>
</div>
</div>
<div className="messages">
<h2>Recent Messages</h2>
<button onClick={loadMessages} className="refresh-button">
Refresh
</button>
{messages.length === 0 ? (
<p>No messages yet. Be the first to post!</p>
) : (
<ul>
{messages.map((message) => (
<li key={message.id}>
<strong>Message #{message.id}:</strong> {message.content}
</li>
))}
</ul>
)}
</div>
</main>
)}
</div>
);
}
export default App;Add Basic Styling
Update src/App.css:
.App {
max-width: 800px;
width: 100%;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
}
.App-header {
text-align: center;
margin-bottom: 40px;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.App-header h1 {
color: white;
margin-bottom: 20px;
font-size: 2.5rem;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.connect-button,
.disconnect-button {
background-color: rgba(255, 255, 255, 0.2);
color: white;
border: 2px solid rgba(255, 255, 255, 0.3);
padding: 12px 28px;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: 600;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.connect-button:hover,
.disconnect-button:hover {
background-color: rgba(255, 255, 255, 0.3);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
}
.post-message {
margin-bottom: 40px;
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.message-input {
display: flex;
gap: 10px;
margin-top: 10px;
}
.message-input input {
flex: 1;
padding: 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
font-size: 16px;
}
.message-input button {
background-color: #10b981;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
.message-input button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
.messages {
padding: 20px;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.refresh-button {
background-color: #6b7280;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-bottom: 20px;
}
.messages ul {
list-style: none;
padding: 0;
}
.messages li {
padding: 10px;
border-bottom: 1px solid #e5e7eb;
margin-bottom: 10px;
}
.messages li:last-child {
border-bottom: none;
}Update the Contract Address
Go back to the Stacks Explorer and find your deployed contract
Copy the contract address (it looks like
ST1ABC...123.message-board)Replace
YOUR_CONTRACT_ADDRESS_HEREin the App.jsx file with your actual contract address and the contract name with the actual name
Run Your App
npm run devVisit http://localhost:5173 and you should see your message board app! Connect your wallet and try posting a message.
Congratulations! 🎉
You've just built your first Stacks application! Here's what you accomplished:
✅ Wrote a Clarity smart contract with data storage and public functions
✅ Deployed the contract to Stacks testnet
✅ Built a React frontend that connects to a Stacks wallet
✅ Integrated your frontend with your smart contract
✅ Posted and read data from the blockchain
Next Steps
Now that you have the basics down, here are some ways to continue your Stacks development journey:
Learn More About Clarity
Clarity Book: Comprehensive guide to Clarity development
Clarity Reference: Complete documentation of Clarity functions
Clarity Crash Course: Quick introduction to Clarity concepts
Explore Advanced Features
Error Handling: Learn about Clarity's
try!andunwrap!functionsAccess Control: Implement admin functions and permissions
Token Standards: Build fungible (SIP-010) and non-fungible (SIP-009) tokens
Development Tools
Clarinet: Local development environment for Clarity
Hiro Platform: Hosted development environment
Stacks Explorer: View transactions and contracts on mainnet
Community Resources
Stacks Discord: Connect with other developers
Stacks Forum: Ask questions and share projects
Stacks GitHub: Contribute to the ecosystem
Last updated
Was this helpful?

