Skip to main content

Aptos dApp Development Guide

Complete guide to building modern decentralized applications on Aptos blockchain using the latest @aptos-labs/ts-sdk and best practices.

Share
Last updated on October 5, 2026
On this page

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​

bash
# 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​

typescript
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 bex.co endpoint
const bexConfig = new AptosConfig({
  fullnode: 'https://aptos-mainnet.bex.co/<access_key>',
  network: Network.MAINNET,
});
const bexAptos = new Aptos(bexConfig);

2. React dApp Setup with Wallet Integration​

typescript
// 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​

typescript
// 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​

typescript
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​

typescript
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​

typescript
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&lt;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​

typescript
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​

typescript
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​

typescript
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​

typescript
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​

typescript
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​

typescript
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​

typescript
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​

typescript
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​

typescript
// __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​

typescript
// __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​

typescript
// 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​

typescript
// 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​

  1. Use TypeScript for better type safety and developer experience
  2. Implement proper error handling for all blockchain interactions
  3. Validate inputs before sending transactions
  4. Simulate transactions before execution when possible
  5. Use React Query for efficient data fetching and caching
  6. Handle wallet connection states gracefully
  7. Test thoroughly with both unit and integration tests
  8. Monitor transactions in production environments
  9. Secure API endpoints using environment variables

Next Steps​

Resources​

Was this page helpful?

One API key for 30+ chains

bex router is a single multi-chain gateway — one key, one bill, metered in compute units instead of an account per network.

Explore bex router