How I migrated my Stacks Exchange AMM from a monolithic structure to clean separation with automated contract synchronization
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.
Key Topics:
Before: Monolithic structure with everything bundled together
# Backend (Contract Development)
stacks-amm/
├── contracts/
├── deployments/
├── frontend/ (Integrated Ecosystem)
├── settings
└── tests
After: Clean separation with automated synchronization
# Backend (Contract Development)
pasifika-stacks-exchange/
├── contracts/
├── deployments/
├── settings/
└── tests/
# Frontend (Integrated Ecosystem)
pasifika-web3-fe/
├── app/
├── deployed_contracts/
├── lib/
├── public/
├── scripts/
└── src/config/
npm install @stacks/connect @stacks/network @stacks/transactions
pasifika-web3-fe/app/stacks-exchange/
├── page.tsx
├── components/
├── hooks/
└── lib/
// 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>
);
}
// 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 };
}
The Key Innovation: Automated script to sync contract addresses from backend deployments to frontend.
// 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": {
"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"
}
}
# Run sync script
npm run sync-contracts
# Verify generated files
ls deployed_contracts/
# - contract-addresses.ts
# - contracts.json
# Test frontend integration
npm run dev
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:
This tutorial is part of the Pasifika Web3 Tech Hub's commitment to sharing knowledge and empowering Pacific Island developers in Stacks blockchain technology.