Skip to main content

Sui dApp Development Guide

Complete guide to building modern decentralized applications on Sui blockchain using the latest @mysten/sui SDK and @mysten/dapp-kit.

Share
Last updated on August 20, 2026

This comprehensive guide covers building modern decentralized applications (dApps) on the Sui blockchain using the latest tools and 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)
  • Sui wallet installed (Sui Wallet, Suiet, or other Sui-compatible wallets)

Core Technologies

Essential Packages

bash
# Core Sui SDK (modular imports)
npm install @mysten/sui
 
# dApp Kit for React applications
npm install @mysten/dapp-kit
 
# Additional utilities
npm install @mysten/dapp-kit @tanstack/react-query

Package Overview

  • @mysten/sui: Core SDK for blockchain interactions
  • @mysten/dapp-kit: React hooks and components for wallet integration
  • @tanstack/react-query: State management for async data (recommended)

Basic Setup

1. Initialize Sui Client

typescript
import { SuiClient, getFullnodeUrl } from '@mysten/sui/client';
 
// Initialize client for different networks
const client = new SuiClient({ 
  url: getFullnodeUrl('mainnet') // 'testnet', 'devnet', 'localnet'
});
 
// Using bex.co endpoint
const bexClient = new SuiClient({
  url: 'https://api.bex.co/sui/<access_key>'
});

2. React dApp Setup with dApp Kit

typescript
// App.tsx
import { SuiClientProvider, WalletProvider } from '@mysten/dapp-kit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { getFullnodeUrl } from '@mysten/sui/client';
 
const queryClient = new QueryClient();
const networks = {
  mainnet: { url: getFullnodeUrl('mainnet') },
  testnet: { url: getFullnodeUrl('testnet') },
};
 
function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <SuiClientProvider networks={networks} defaultNetwork="testnet">
        <WalletProvider>
          <MyDApp />
        </WalletProvider>
      </SuiClientProvider>
    </QueryClientProvider>
  );
}

3. Wallet Connection Component

typescript
// WalletConnection.tsx
import { ConnectButton, useCurrentAccount } from '@mysten/dapp-kit';
 
export function WalletConnection() {
  const currentAccount = useCurrentAccount();
 
  return (
    <div className="wallet-section">
      <ConnectButton />
      {currentAccount && (
        <div>
          <p>Connected: {currentAccount.address}</p>
        </div>
      )}
    </div>
  );
}

Working with Transactions

Building and Executing Transactions

typescript
import { Transaction } from '@mysten/sui/transactions';
import { useSignAndExecuteTransaction, useSuiClient } from '@mysten/dapp-kit';
 
export function TransferSui() {
  const client = useSuiClient();
  const { mutate: signAndExecute } = useSignAndExecuteTransaction();
 
  const transferSui = async (recipient: string, amount: number) => {
    const tx = new Transaction();
    
    // Convert SUI to MIST (1 SUI = 1,000,000,000 MIST)
    const amountMIST = amount * 1_000_000_000;
    
    // Split coins and transfer
    const [coin] = tx.splitCoins(tx.gas, [amountMIST]);
    tx.transferObjects([coin], recipient);
    
    // Execute transaction
    signAndExecute(
      { transaction: tx },
      {
        onSuccess: (result) => {
          console.log('Transfer successful:', result.digest);
        },
        onError: (error) => {
          console.error('Transfer failed:', error);
        },
      }
    );
  };
 
  return (
    <button onClick={() => transferSui('0x...', 1)}>
      Send 1 SUI
    </button>
  );
}

Move Call Transactions

typescript
import { Transaction } from '@mysten/sui/transactions';
 
export function MoveCallExample() {
  const { mutate: signAndExecute } = useSignAndExecuteTransaction();
 
  const callMoveFunction = async () => {
    const tx = new Transaction();
    
    // Call a Move function
    tx.moveCall({
      target: '0x2::coin::split',
      arguments: [
        tx.object('0x...'), // coin object
        tx.pure.u64(1000000000), // amount in MIST
      ],
    });
    
    signAndExecute({ transaction: tx });
  };
 
  return <button onClick={callMoveFunction}>Call Move Function</button>;
}

Data Fetching Patterns

Using React Query with Sui Data

typescript
import { useQuery } from '@tanstack/react-query';
import { useSuiClient } from '@mysten/dapp-kit';
 
export function AccountBalance({ address }: { address: string }) {
  const client = useSuiClient();
 
  const { data: balance, isLoading, error } = useQuery({
    queryKey: ['balance', address],
    queryFn: () => client.getBalance({ owner: address }),
    enabled: !!address,
    refetchInterval: 10000, // Refetch every 10 seconds
  });
 
  if (isLoading) return <div>Loading balance...</div>;
  if (error) return <div>Error loading balance</div>;
 
  return (
    <div>
      Balance: {balance ? Number(balance.totalBalance) / 1_000_000_000 : 0} SUI
    </div>
  );
}

Fetching Objects and Events

typescript
export function ObjectsDisplay({ address }: { address: string }) {
  const client = useSuiClient();
 
  const { data: objects } = useQuery({
    queryKey: ['objects', address],
    queryFn: () => client.getOwnedObjects({
      owner: address,
      options: {
        showContent: true,
        showType: true,
      },
    }),
    enabled: !!address,
  });
 
  return (
    <div>
      <h3>Owned Objects</h3>
      {objects?.data.map((obj) => (
        <div key={obj.data?.objectId}>
          <p>Object ID: {obj.data?.objectId}</p>
          <p>Type: {obj.data?.type}</p>
        </div>
      ))}
    </div>
  );
}

Event Handling and Real-time Updates

Listening to Events

typescript
import { useEffect, useState } from 'react';
import { useSuiClient } from '@mysten/dapp-kit';
 
export function EventListener() {
  const client = useSuiClient();
  const [events, setEvents] = useState<any[]>([]);
 
  useEffect(() => {
    const fetchEvents = async () => {
      try {
        const eventData = await client.queryEvents({
          query: { MoveModule: { package: '0x2', module: 'coin' } },
          limit: 10,
          order: 'descending',
        });
        setEvents(eventData.data);
      } catch (error) {
        console.error('Error fetching events:', error);
      }
    };
 
    fetchEvents();
    
    // Poll for new events every 5 seconds
    const interval = setInterval(fetchEvents, 5000);
    return () => clearInterval(interval);
  }, [client]);
 
  return (
    <div>
      <h3>Recent Events</h3>
      {events.map((event, index) => (
        <div key={index}>
          <p>Type: {event.type}</p>
          <p>Timestamp: {event.timestampMs}</p>
        </div>
      ))}
    </div>
  );
}

Error Handling Best Practices

Comprehensive Error Handling

typescript
import { SuiError } from '@mysten/sui/client';
 
export function RobustTransaction() {
  const { mutate: signAndExecute } = useSignAndExecuteTransaction();
 
  const executeWithErrorHandling = async () => {
    const tx = new Transaction();
    // ... build transaction
 
    signAndExecute(
      { transaction: tx },
      {
        onSuccess: (result) => {
          if (result.effects?.status?.status === 'success') {
            console.log('Transaction succeeded:', result.digest);
          } else {
            console.error('Transaction failed:', result.effects?.status);
          }
        },
        onError: (error) => {
          if (error instanceof SuiError) {
            // Handle Sui-specific errors
            console.error('Sui error:', error.message);
          } else if (error.message.includes('User rejected')) {
            // Handle user rejection
            console.log('User cancelled transaction');
          } else {
            // Handle other errors
            console.error('Unexpected error:', error);
          }
        },
      }
    );
  };
 
  return <button onClick={executeWithErrorHandling}>Execute Transaction</button>;
}

Network Error Handling

typescript
export function useResilientQuery<T>(
  queryKey: any[],
  queryFn: () => Promise<T>,
  options?: any
) {
  return useQuery({
    queryKey,
    queryFn,
    retry: (failureCount, error) => {
      // Retry up to 3 times for network errors
      if (failureCount < 3 && error.message.includes('network')) {
        return true;
      }
      return false;
    },
    retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    ...options,
  });
}

Performance Optimization

Efficient Data Loading

typescript
// Batch multiple queries for better performance
export function BatchedDataLoader({ addresses }: { addresses: string[] }) {
  const client = useSuiClient();
 
  const { data: balances } = useQuery({
    queryKey: ['batchedBalances', addresses],
    queryFn: async () => {
      // Use Promise.all for concurrent requests
      const balancePromises = addresses.map(address => 
        client.getBalance({ owner: address })
      );
      return Promise.all(balancePromises);
    },
    enabled: addresses.length > 0,
  });
 
  return (
    <div>
      {balances?.map((balance, index) => (
        <div key={addresses[index]}>
          Address: {addresses[index]} - 
          Balance: {Number(balance.totalBalance) / 1_000_000_000} SUI
        </div>
      ))}
    </div>
  );
}

Memoization for Heavy Computations

typescript
import { useMemo } from 'react';
 
export function ProcessedData({ objects }: { objects: any[] }) {
  const processedData = useMemo(() => {
    return objects
      .filter(obj => obj.data?.type?.includes('::coin::'))
      .map(obj => ({
        id: obj.data.objectId,
        balance: obj.data.content?.fields?.balance || 0,
      }))
      .sort((a, b) => b.balance - a.balance);
  }, [objects]);
 
  return (
    <div>
      {processedData.map(item => (
        <div key={item.id}>
          Coin: {item.id} - Balance: {item.balance}
        </div>
      ))}
    </div>
  );
}

Testing Strategies

Unit Testing with Jest

typescript
// __tests__/utils.test.ts
import { describe, it, expect } from '@jest/globals';
import { formatSuiAmount, validateSuiAddress } from '../utils';
 
describe('Sui utilities', () => {
  it('should format SUI amounts correctly', () => {
    expect(formatSuiAmount(1000000000)).toBe('1.00');
    expect(formatSuiAmount(1500000000)).toBe('1.50');
  });
 
  it('should validate Sui addresses', () => {
    expect(validateSuiAddress('0x1')).toBe(true);
    expect(validateSuiAddress('invalid')).toBe(false);
  });
});

Integration Testing

typescript
// __tests__/integration.test.ts
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient } from '@tanstack/react-query';
import { SuiClient } from '@mysten/sui/client';
import { WalletConnection } from '../components/WalletConnection';
 
describe('Wallet Integration', () => {
  it('should display connection status', async () => {
    render(<WalletConnection />);
    
    await waitFor(() => {
      expect(screen.getByText('Connect Wallet')).toBeInTheDocument();
    });
  });
});

Deployment Considerations

Environment Configuration

typescript
// config/sui.ts
export const getSuiConfig = () => {
  const environment = process.env.NODE_ENV;
  
  switch (environment) {
    case 'production':
      return {
        network: 'mainnet',
        rpcUrl: process.env.NEXT_PUBLIC_SUI_MAINNET_URL,
      };
    case 'staging':
      return {
        network: 'testnet',
        rpcUrl: process.env.NEXT_PUBLIC_SUI_TESTNET_URL,
      };
    default:
      return {
        network: 'devnet',
        rpcUrl: getFullnodeUrl('devnet'),
      };
  }
};

Production Optimizations

typescript
// Use environment variables for API keys
const client = new SuiClient({
  url: process.env.NEXT_PUBLIC_BEX_SUI_URL || getFullnodeUrl('mainnet')
});
 
// Implement proper error boundaries
export class SuiErrorBoundary extends React.Component {
  constructor(props: any) {
    super(props);
    this.state = { hasError: false };
  }
 
  static getDerivedStateFromError(error: Error) {
    return { hasError: true };
  }
 
  componentDidCatch(error: Error, errorInfo: any) {
    console.error('Sui dApp error:', error, errorInfo);
    // Log to error reporting service
  }
 
  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong with the Sui connection.</h1>;
    }
 
    return this.props.children;
  }
}

Best Practices Summary

  1. Use TypeScript for better type safety and developer experience
  2. Implement proper error handling for all blockchain interactions
  3. Cache data effectively using React Query or similar libraries
  4. Test thoroughly with both unit and integration tests
  5. Monitor performance and optimize data fetching patterns
  6. Handle network failures gracefully with retry logic
  7. Secure API keys using environment variables
  8. Use modular imports from @mysten/sui for smaller bundle sizes

Next Steps

Resources

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