Aptos dApp Development Guide
This comprehensive guide covers building modern decentralized applications (dApps) on the Aptos blockchain using the latest @aptos-labs/ts-sdk and development best practices for 2025.
Prerequisites
- Node.js 18+ for running the development environment
- Basic TypeScript/JavaScript knowledge for frontend development
- Understanding of React for UI components (optional but recommended)
- Aptos wallet installed (Petra, Pontem, or other Aptos-compatible wallets)
Core Technologies
Essential Packages
# Core Aptos SDK
npm install @aptos-labs/ts-sdk
# React integration (optional)
npm install react react-dom @types/react @types/react-dom
# State management
npm install @tanstack/react-query
# UI components (optional)
npm install @aptos-labs/wallet-adapter-react
Package Overview
- @aptos-labs/ts-sdk: Core SDK for blockchain interactions
- @aptos-labs/wallet-adapter-react: React hooks for wallet integration
- @tanstack/react-query: State management for async data
Basic Setup
1. Initialize Aptos Client
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';
// Initialize for different networks
const config = new AptosConfig({
network: Network.MAINNET // Network.TESTNET, Network.DEVNET
});
const aptos = new Aptos(config);
// Using BlockEden.xyz endpoint
const blockEdenConfig = new AptosConfig({
fullnode: 'https://aptos-mainnet.blockeden.xyz/<access_key>',
network: Network.MAINNET,
});
const blockEdenAptos = new Aptos(blockEdenConfig);
2. React dApp Setup with Wallet Integration
// App.tsx
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WalletProvider } from '@aptos-labs/wallet-adapter-react';
import { PetraWallet } from 'petra-plugin-wallet-adapter';
import { PontemWallet } from '@pontem/wallet-adapter-plugin';
const queryClient = new QueryClient();
const wallets = [
new PetraWallet(),
new PontemWallet(),
];
function App() {
return (
<QueryClientProvider client={queryClient}>
<WalletProvider wallets={wallets} autoConnect={true}>
<MyDApp />
</WalletProvider>
</QueryClientProvider>
);
}
export default App;
3. Wallet Connection Component
// WalletConnection.tsx
import React from 'react';
import { useWallet } from '@aptos-labs/wallet-adapter-react';
export function WalletConnection() {
const {
connect,
disconnect,
account,
connected,
wallets,
wallet
} = useWallet();
const handleConnect = async (walletName: string) => {
try {
await connect(walletName);
} catch (error) {
console.error('Failed to connect wallet:', error);
}
};
const handleDisconnect = async () => {
try {
await disconnect();
} catch (error) {
console.error('Failed to disconnect wallet:', error);
}
};
if (connected && account) {
return (
<div className="wallet-connected">
<p>Connected: {account.address}</p>
<p>Wallet: {wallet?.name}</p>
<button onClick={handleDisconnect}>Disconnect</button>
</div>
);
}
return (
<div className="wallet-selection">
<h3>Connect Your Wallet</h3>
{wallets.map((wallet) => (
<button
key={wallet.name}
onClick={() => handleConnect(wallet.name)}
disabled={!wallet.readyState}
>
Connect {wallet.name}
</button>
))}
</div>
);
}
Working with Transactions
Basic APT Transfer
import { Account, Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';
import { useWallet } from '@aptos-labs/wallet-adapter-react';
export function TransferAPT() {
const { account, signAndSubmitTransaction } = useWallet();
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
const transferAPT = async (recipient: string, amount: number) => {
if (!account) {
throw new Error('Wallet not connected');
}
try {
const transaction = await aptos.transaction.build.simple({
sender: account.address,
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [
recipient, // recipient address
amount * 100000000, // amount in Octas (1 APT = 100,000,000 Octas)
],
},
});
const response = await signAndSubmitTransaction({
transaction,
});
// Wait for transaction confirmation
const executedTransaction = await aptos.waitForTransaction({
transactionHash: response.hash,
});
console.log('Transfer successful:', executedTransaction);
return executedTransaction;
} catch (error) {
console.error('Transfer failed:', error);
throw error;
}
};
return (
<div>
<button onClick={() => transferAPT('0x...', 1)}>
Send 1 APT
</button>
</div>
);
}
Multi-Agent Transactions
export function MultiAgentTransaction() {
const { account, signAndSubmitTransaction } = useWallet();
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
const createMultiAgentTx = async (secondaryAccount: string) => {
if (!account) throw new Error('Wallet not connected');
try {
const transaction = await aptos.transaction.build.multiAgent({
sender: account.address,
secondarySignerAddresses: [secondaryAccount],
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [
account.address, // Transfer back to primary account
1000000, // 0.01 APT
],
},
});
const response = await signAndSubmitTransaction({
transaction,
});
return await aptos.waitForTransaction({
transactionHash: response.hash,
});
} catch (error) {
console.error('Multi-agent transaction failed:', error);
throw error;
}
};
return (
<button onClick={() => createMultiAgentTx('0x...')}>
Create Multi-Agent Transaction
</button>
);
}
Data Fetching Patterns
Account Information and Balance
import { useQuery } from '@tanstack/react-query';
import { Aptos, AptosConfig, Network } from '@aptos-labs/ts-sdk';
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
export function AccountBalance({ address }: { address: string }) {
const { data: balance, isLoading, error } = useQuery({
queryKey: ['balance', address],
queryFn: async () => {
const resources = await aptos.getAccountResources({
accountAddress: address,
});
const coinStore = resources.find(
(resource) => resource.type === '0x1::coin::CoinStore<0x1::aptos_coin::AptosCoin>'
);
if (coinStore?.data) {
return (coinStore.data as any).coin.value;
}
return '0';
},
enabled: !!address,
refetchInterval: 10000, // Refresh every 10 seconds
});
if (isLoading) return <div>Loading balance...</div>;
if (error) return <div>Error loading balance</div>;
const aptBalance = balance ? Number(balance) / 100000000 : 0;
return (
<div>
Balance: {aptBalance.toFixed(8)} APT
</div>
);
}
Account Resources and Modules
export function AccountResources({ address }: { address: string }) {
const { data: resources } = useQuery({
queryKey: ['resources', address],
queryFn: () => aptos.getAccountResources({ accountAddress: address }),
enabled: !!address,
});
const { data: modules } = useQuery({
queryKey: ['modules', address],
queryFn: () => aptos.getAccountModules({ accountAddress: address }),
enabled: !!address,
});
return (
<div>
<h3>Account Resources</h3>
<div>Total Resources: {resources?.length || 0}</div>
<div>Total Modules: {modules?.length || 0}</div>
{resources?.slice(0, 5).map((resource, index) => (
<div key={index}>
<strong>Type:</strong> {resource.type}
</div>
))}
</div>
);
}
Transaction History
export function TransactionHistory({ address }: { address: string }) {
const { data: transactions, isLoading } = useQuery({
queryKey: ['transactions', address],
queryFn: () => aptos.getAccountTransactions({
accountAddress: address,
options: {
limit: 10,
orderBy: [{ transaction_version: 'desc' }],
},
}),
enabled: !!address,
refetchInterval: 30000, // Refresh every 30 seconds
});
if (isLoading) return <div>Loading transactions...</div>;
return (
<div>
<h3>Recent Transactions</h3>
{transactions?.map((tx) => (
<div key={tx.version} className="transaction-item">
<div>Version: {tx.version}</div>
<div>Hash: {tx.hash}</div>
<div>Type: {tx.type}</div>
<div>Success: {tx.success ? 'Yes' : 'No'}</div>
</div>
))}
</div>
);
}
Event Handling and Monitoring
Event Listening with React
import { useEffect, useState } from 'react';
import { Event } from '@aptos-labs/ts-sdk';
export function EventMonitor({ eventType }: { eventType: string }) {
const [events, setEvents] = useState<Event[]>([]);
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
useEffect(() => {
const fetchEvents = async () => {
try {
const eventData = await aptos.getEvents({
eventType,
options: {
limit: 10,
orderBy: [{ transaction_version: 'desc' }],
},
});
setEvents(eventData);
} catch (error) {
console.error('Error fetching events:', error);
}
};
fetchEvents();
// Poll for new events
const interval = setInterval(fetchEvents, 5000);
return () => clearInterval(interval);
}, [eventType]);
return (
<div>
<h3>Events: {eventType}</h3>
{events.map((event, index) => (
<div key={index} className="event-item">
<div>Version: {event.transaction_version}</div>
<div>Sequence: {event.sequence_number}</div>
<div>Data: {JSON.stringify(event.data)}</div>
</div>
))}
</div>
);
}
Custom Hook for Event Subscription
import { useQuery } from '@tanstack/react-query';
export function useEvents(eventType: string, enabled: boolean = true) {
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
return useQuery({
queryKey: ['events', eventType],
queryFn: () => aptos.getEvents({
eventType,
options: {
limit: 20,
orderBy: [{ transaction_version: 'desc' }],
},
}),
enabled,
refetchInterval: 5000,
});
}
Error Handling and Validation
Comprehensive Error Handling
import { AptosApiError } from '@aptos-labs/ts-sdk';
export function RobustTransaction() {
const { signAndSubmitTransaction } = useWallet();
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
const executeTransaction = async (transaction: any) => {
try {
// Simulate transaction first
const simulation = await aptos.transaction.simulate.simple({
signerPublicKey: account.publicKey,
transaction,
});
if (!simulation[0].success) {
throw new Error(`Simulation failed: ${simulation[0].vm_status}`);
}
// Execute the transaction
const response = await signAndSubmitTransaction({ transaction });
// Wait for confirmation
const confirmedTx = await aptos.waitForTransaction({
transactionHash: response.hash,
options: {
checkSuccess: true,
},
});
return confirmedTx;
} catch (error) {
if (error instanceof AptosApiError) {
console.error('Aptos API Error:', error.message);
console.error('Status:', error.status);
console.error('Data:', error.data);
} else if (error.message.includes('User rejected')) {
console.log('User cancelled transaction');
} else {
console.error('Unexpected error:', error);
}
throw error;
}
};
return (
<button onClick={() => executeTransaction(/* transaction */)}>
Execute Transaction
</button>
);
}
Input Validation
import { AccountAddress } from '@aptos-labs/ts-sdk';
export function validateAptosAddress(address: string): boolean {
try {
AccountAddress.fromString(address);
return true;
} catch {
return false;
}
}
export function validateAmount(amount: string): { isValid: boolean; error?: string } {
const numAmount = parseFloat(amount);
if (isNaN(numAmount)) {
return { isValid: false, error: 'Invalid number format' };
}
if (numAmount <= 0) {
return { isValid: false, error: 'Amount must be positive' };
}
if (numAmount > 1000000) {
return { isValid: false, error: 'Amount too large' };
}
return { isValid: true };
}
Performance Optimization
Batch Requests
export function BatchedAccountData({ addresses }: { addresses: string[] }) {
const aptos = new Aptos(new AptosConfig({ network: Network.TESTNET }));
const { data: accountData } = useQuery({
queryKey: ['batchedAccounts', addresses],
queryFn: async () => {
// Batch multiple account queries
const accountPromises = addresses.map(address =>
aptos.getAccount({ accountAddress: address })
.catch(error => ({ error, address }))
);
const results = await Promise.allSettled(accountPromises);
return results.map((result, index) => ({
address: addresses[index],
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? result.reason : null,
}));
},
enabled: addresses.length > 0,
});
return (
<div>
<h3>Account Data</h3>
{accountData?.map((item, index) => (
<div key={index}>
<strong>{item.address}:</strong>
{item.error ? (
<span> Error loading account</span>
) : (
<span> Sequence: {item.data?.sequence_number}</span>
)}
</div>
))}
</div>
);
}
Caching and Memoization
import { useMemo } from 'react';
export function ProcessedTransactions({ transactions }: { transactions: any[] }) {
const processedData = useMemo(() => {
return transactions
.filter(tx => tx.success)
.map(tx => ({
hash: tx.hash,
version: tx.version,
timestamp: new Date(Number(tx.timestamp) / 1000),
gasUsed: tx.gas_used,
}))
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
}, [transactions]);
return (
<div>
{processedData.map(tx => (
<div key={tx.hash}>
<div>Hash: {tx.hash}</div>
<div>Gas Used: {tx.gasUsed}</div>
<div>Time: {tx.timestamp.toLocaleString()}</div>
</div>
))}
</div>
);
}
Testing Strategies
Unit Testing
// __tests__/aptosUtils.test.ts
import { describe, it, expect } from '@jest/globals';
import { validateAptosAddress, validateAmount } from '../utils/validation';
describe('Aptos Utilities', () => {
describe('validateAptosAddress', () => {
it('should validate correct Aptos addresses', () => {
expect(validateAptosAddress('0x1')).toBe(true);
expect(validateAptosAddress('0x1::coin::CoinStore')).toBe(false);
});
it('should reject invalid addresses', () => {
expect(validateAptosAddress('invalid')).toBe(false);
expect(validateAptosAddress('')).toBe(false);
});
});
describe('validateAmount', () => {
it('should validate positive numbers', () => {
expect(validateAmount('1.5')).toEqual({ isValid: true });
expect(validateAmount('100')).toEqual({ isValid: true });
});
it('should reject invalid amounts', () => {
expect(validateAmount('-1')).toEqual({
isValid: false,
error: 'Amount must be positive'
});
expect(validateAmount('abc')).toEqual({
isValid: false,
error: 'Invalid number format'
});
});
});
});
Integration Testing
// __tests__/integration.test.ts
import { Aptos, AptosConfig, Network, Account } from '@aptos-labs/ts-sdk';
describe('Aptos Integration Tests', () => {
let aptos: Aptos;
let account: Account;
beforeAll(async () => {
aptos = new Aptos(new AptosConfig({ network: Network.DEVNET }));
account = Account.generate();
// Fund account for testing
await aptos.fundAccount({
accountAddress: account.accountAddress,
amount: 100000000
});
});
it('should transfer APT successfully', async () => {
const recipient = Account.generate();
const transaction = await aptos.transaction.build.simple({
sender: account.accountAddress,
data: {
function: '0x1::aptos_account::transfer',
functionArguments: [recipient.accountAddress, 1000000],
},
});
const response = await aptos.signAndSubmitTransaction({
signer: account,
transaction,
});
const confirmedTx = await aptos.waitForTransaction({
transactionHash: response.hash,
});
expect(confirmedTx.success).toBe(true);
});
});
Deployment Configuration
Environment-Specific Setup
// config/aptos.ts
import { AptosConfig, Network } from '@aptos-labs/ts-sdk';
export const getAptosConfig = () => {
const environment = process.env.NODE_ENV;
switch (environment) {
case 'production':
return new AptosConfig({
network: Network.MAINNET,
fullnode: process.env.NEXT_PUBLIC_APTOS_MAINNET_URL,
});
case 'staging':
return new AptosConfig({
network: Network.TESTNET,
fullnode: process.env.NEXT_PUBLIC_APTOS_TESTNET_URL,
});
default:
return new AptosConfig({
network: Network.DEVNET,
});
}
};
Production Monitoring
// utils/monitoring.ts
export function logTransaction(hash: string, status: 'success' | 'failure', error?: any) {
const logData = {
transactionHash: hash,
status,
timestamp: new Date().toISOString(),
error: error?.message,
};
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.log('Transaction log:', logData);
}
// Send to monitoring service in production
if (process.env.NODE_ENV === 'production') {
// Example: send to DataDog, Sentry, etc.
// monitoringService.log('aptos_transaction', logData);
}
}
Best Practices Summary
- Use TypeScript for better type safety and developer experience
- Implement proper error handling for all blockchain interactions
- Validate inputs before sending transactions
- Simulate transactions before execution when possible
- Use React Query for efficient data fetching and caching
- Handle wallet connection states gracefully
- Test thoroughly with both unit and integration tests
- Monitor transactions in production environments
- Secure API endpoints using environment variables
Next Steps
- Explore the Aptos Move Integration Guide for smart contract interactions
- Learn about Multi-Chain dApp Architecture for cross-chain applications
- Implement proper security measures for production deployment