How I migrated my Stacks Exchange AMM from a monolithic structure to clean separation with automated contract synchronization How I migrated my Stacks Exchange AMM from a monolithic structure to clean separation with automated contract synchronization Introduction Building decentralized applications often starts with everything bundled together. As projects grow and integrate into larger ecosystems, you need clean separation of concerns. This guide covers how I separated my Stacks Exchange AMM frontend from its backend, integrated it into the Pasifika Web3 Tech Hub ecosystem, and created automated contract address synchronization. Note: This project was originally forked from LearnWeb3DAO/stacks-amm to be extended for Pacific Island communities. Note LearnWeb3DAO/stacks-amm Key Topics: Key Topics: Frontend/backend separation strategies Automated contract address synchronization Integration with existing ecosystems Production-ready optimization Frontend/backend separation strategies Automated contract address synchronization Integration with existing ecosystems Production-ready optimization Architecture Overview Before: Monolithic structure with everything bundled together Before: # Backend (Contract Development) stacks-amm/ ├── contracts/ ├── deployments/ ├── frontend/ (Integrated Ecosystem) ├── settings └── tests # Backend (Contract Development) stacks-amm/ ├── contracts/ ├── deployments/ ├── frontend/ (Integrated Ecosystem) ├── settings └── tests After: Clean separation with automated synchronization After: # Backend (Contract Development) pasifika-stacks-exchange/ ├── contracts/ ├── deployments/ ├── settings/ └── tests/ # Frontend (Integrated Ecosystem) pasifika-web3-fe/ ├── app/ ├── deployed_contracts/ ├── lib/ ├── public/ ├── scripts/ └── src/config/ # Backend (Contract Development) pasifika-stacks-exchange/ ├── contracts/ ├── deployments/ ├── settings/ └── tests/ # Frontend (Integrated Ecosystem) pasifika-web3-fe/ ├── app/ ├── deployed_contracts/ ├── lib/ ├── public/ ├── scripts/ └── src/config/ Key Migration Steps 1. Analysis & Planning Components: AMM UI (Swap, Liquidity, Pools) Dependencies: Stacks libraries, React/Next.js version differences Strategy: Preserve functionality while adopting new theming Components: AMM UI (Swap, Liquidity, Pools) Components Dependencies: Stacks libraries, React/Next.js version differences Dependencies Strategy: Preserve functionality while adopting new theming Strategy 2. Dependencies Setup npm install @stacks/connect @stacks/network @stacks/transactions npm install @stacks/connect @stacks/network @stacks/transactions 3. Directory Structure pasifika-web3-fe/app/stacks-exchange/ ├── page.tsx ├── components/ ├── hooks/ └── lib/ pasifika-web3-fe/app/stacks-exchange/ ├── page.tsx ├── components/ ├── hooks/ └── lib/ Core Implementation Main AMM Page Integration // app/stacks-exchange/page.tsx export default function StacksExchange() { const { isDarkMode } = useDarkMode(); const [pools, setPools] = useState([]); const [activeTab, setActiveTab] = useState("swap"); return ( <div className={`container ${isDarkMode ? 'dark' : 'light'}`}> {/* Pasifika Header */} <div className="header"> <div className="logo"> <Image src="/pasifika.png" alt="Pasifika" /> <span>Pasifika</span> </div> </div> {/* AMM Interface */} <div className="amm-container"> <div className="tab-navigation"> {["swap", "add-liquidity", "pools"].map((tab) => ( <button onClick={() => setActiveTab(tab)}> {tab.toUpperCase()} </button> ))} </div> {/* Tab Content */} {activeTab === "swap" && <Swap pools={pools} />} {activeTab === "add-liquidity" && <AddLiquidity pools={pools} />} {activeTab === "pools" && <PoolsList pools={pools} />} </div> </div> ); } // app/stacks-exchange/page.tsx export default function StacksExchange() { const { isDarkMode } = useDarkMode(); const [pools, setPools] = useState([]); const [activeTab, setActiveTab] = useState("swap"); return ( <div className={`container ${isDarkMode ? 'dark' : 'light'}`}> {/* Pasifika Header */} <div className="header"> <div className="logo"> <Image src="/pasifika.png" alt="Pasifika" /> <span>Pasifika</span> </div> </div> {/* AMM Interface */} <div className="amm-container"> <div className="tab-navigation"> {["swap", "add-liquidity", "pools"].map((tab) => ( <button onClick={() => setActiveTab(tab)}> {tab.toUpperCase()} </button> ))} </div> {/* Tab Content */} {activeTab === "swap" && <Swap pools={pools} />} {activeTab === "add-liquidity" && <AddLiquidity pools={pools} />} {activeTab === "pools" && <PoolsList pools={pools} />} </div> </div> ); } Stacks Wallet Integration // hooks/use-stacks.ts export function useStacks() { const [userData, setUserData] = useState(null); const appConfig = useMemo(() => new AppConfig(["store_write"]), []); const userSession = useMemo(() => new UserSession({ appConfig }), [appConfig]); const connectWallet = useCallback(() => { showConnect({ appDetails, userSession }); }, [userSession]); return { userData, connectWallet, handleCreatePool, handleSwap }; } // hooks/use-stacks.ts export function useStacks() { const [userData, setUserData] = useState(null); const appConfig = useMemo(() => new AppConfig(["store_write"]), []); const userSession = useMemo(() => new UserSession({ appConfig }), [appConfig]); const connectWallet = useCallback(() => { showConnect({ appDetails, userSession }); }, [userSession]); return { userData, connectWallet, handleCreatePool, handleSwap }; } Contract Address Synchronization The Key Innovation: Automated script to sync contract addresses from backend deployments to frontend. The Key Innovation How It Works Reads Clarinet deployment YAML files Extracts contract addresses and metadata Generates TypeScript definitions for frontend Saves JSON files for runtime use Reads Clarinet deployment YAML files Extracts contract addresses and metadata Generates TypeScript definitions for frontend Saves JSON files for runtime use Core Sync Script // scripts/save-contract-addresses.js const fs = require('fs'); const yaml = require('js-yaml'); // Parse Clarinet deployment files function extractContractInfo(deploymentPlan, network) { const contracts = {}; deploymentPlan.genesis.plan.batches.forEach(batch => { batch.transactions?.forEach(transaction => { if (transaction['contract-publish']) { const contract = transaction['contract-publish']; contracts[contract['contract-name']] = { address: contract['expected-sender'], network: network, deployedAt: new Date().toISOString() }; } }); }); return contracts; } // Generate TypeScript definitions function generateTypeScriptDefinitions(contracts) { const contractNames = Object.keys(contracts); return ` export const DEPLOYED_CONTRACTS = ${JSON.stringify(contracts, null, 2)}; // Contract addresses ${contractNames.map(name => `export const ${name.toUpperCase()}_CONTRACT = "${contracts[name].address}.${name}";` ).join('\n')} `; } // Main sync function async function main() { const deploymentFile = 'deployments/default.testnet-plan.yaml'; const deploymentPlan = yaml.load(fs.readFileSync(deploymentFile, 'utf8')); const contracts = extractContractInfo(deploymentPlan, 'testnet'); const tsContent = generateTypeScriptDefinitions(contracts); // Save to frontend fs.writeFileSync('deployed_contracts/contract-addresses.ts', tsContent); fs.writeFileSync('deployed_contracts/contracts.json', JSON.stringify(contracts, null, 2)); console.log('Contract addresses synchronized!'); } if (require.main === module) main(); // scripts/save-contract-addresses.js const fs = require('fs'); const yaml = require('js-yaml'); // Parse Clarinet deployment files function extractContractInfo(deploymentPlan, network) { const contracts = {}; deploymentPlan.genesis.plan.batches.forEach(batch => { batch.transactions?.forEach(transaction => { if (transaction['contract-publish']) { const contract = transaction['contract-publish']; contracts[contract['contract-name']] = { address: contract['expected-sender'], network: network, deployedAt: new Date().toISOString() }; } }); }); return contracts; } // Generate TypeScript definitions function generateTypeScriptDefinitions(contracts) { const contractNames = Object.keys(contracts); return ` export const DEPLOYED_CONTRACTS = ${JSON.stringify(contracts, null, 2)}; // Contract addresses ${contractNames.map(name => `export const ${name.toUpperCase()}_CONTRACT = "${contracts[name].address}.${name}";` ).join('\n')} `; } // Main sync function async function main() { const deploymentFile = 'deployments/default.testnet-plan.yaml'; const deploymentPlan = yaml.load(fs.readFileSync(deploymentFile, 'utf8')); const contracts = extractContractInfo(deploymentPlan, 'testnet'); const tsContent = generateTypeScriptDefinitions(contracts); // Save to frontend fs.writeFileSync('deployed_contracts/contract-addresses.ts', tsContent); fs.writeFileSync('deployed_contracts/contracts.json', JSON.stringify(contracts, null, 2)); console.log('Contract addresses synchronized!'); } if (require.main === module) main(); Package.json Integration { "scripts": { "sync-contracts": "node scripts/save-contract-addresses.js", "dev": "npm run sync-contracts && next dev", "build": "npm run sync-contracts && next build" }, "devDependencies": { "js-yaml": "^4.1.0" } } { "scripts": { "sync-contracts": "node scripts/save-contract-addresses.js", "dev": "npm run sync-contracts && next dev", "build": "npm run sync-contracts && next build" }, "devDependencies": { "js-yaml": "^4.1.0" } } Testing & Verification # Run sync script npm run sync-contracts # Verify generated files ls deployed_contracts/ # - contract-addresses.ts # - contracts.json # Test frontend integration npm run dev # Run sync script npm run sync-contracts # Verify generated files ls deployed_contracts/ # - contract-addresses.ts # - contracts.json # Test frontend integration npm run dev Results and Benefits What I Achieved Clean Separation: Frontend and backend are now properly separated Automated Sync: Contract addresses update automatically Theme Integration: Seamless Pasifika branding Type Safety: Full TypeScript support for contract addresses Production Ready: Zero ESLint warnings, optimized performance Scalable Architecture: Easy to add new contracts and features Clean Separation: Frontend and backend are now properly separated Clean Separation Automated Sync: Contract addresses update automatically Automated Sync Theme Integration: Seamless Pasifika branding Theme Integration Type Safety: Full TypeScript support for contract addresses Type Safety Production Ready: Zero ESLint warnings, optimized performance Production Ready Scalable Architecture: Easy to add new contracts and features Scalable Architecture Conclusion Separating your DApp frontend from the backend is crucial for scalability, maintainability, and team collaboration. By implementing automated contract address synchronization, you ensure that your frontend always stays in sync with your latest contract deployments. The approach I've outlined here provides: Clean architecture with proper separation of concerns Automated tooling to reduce manual work and errors Type safety for better developer experience Scalable patterns that grow with your project Clean architecture with proper separation of concerns Clean architecture Automated tooling to reduce manual work and errors Automated tooling Type safety for better developer experience Type safety Scalable patterns that grow with your project Scalable patterns Resources Live Demo: Pasifika Stacks Exchange AMM Stacks.js Documentation Clarinet Documentation Next.js App Router LearnWeb3DAO/stacks-amm Pasifika Web3 Tech Hub Live Demo: Pasifika Stacks Exchange AMM Live Demo: Pasifika Stacks Exchange AMM Stacks.js Documentation Stacks.js Documentation Clarinet Documentation Clarinet Documentation Next.js App Router Next.js App Router LearnWeb3DAO/stacks-amm LearnWeb3DAO/stacks-amm Pasifika Web3 Tech Hub Pasifika Web3 Tech Hub This tutorial is part of the Pasifika Web3 Tech Hub's commitment to sharing knowledge and empowering Pacific Island developers in Stacks blockchain technology. This tutorial is part of the Pasifika Web3 Tech Hub's commitment to sharing knowledge and empowering Pacific Island developers in Stacks blockchain technology.