- Introduction — Why Refactor?
- Architecture Overview
- Shared Module —
donationApi.js - API Chaining — The Dual-Backend Pattern
- Urgency System
- SRP in Action — Real Examples
- APCSP Big Ideas Mapping
- Pages Refactored
- Conclusion
Introduction — Why Refactor?
Hunger Heroes started as a rapid prototype — every page had its own copy of fetch logic, error handling, and UI helpers. As the platform grew to 8 feature pages connecting to two backends (Flask + Spring Boot), duplicated code became a maintenance nightmare. One bug fix required editing the same function in 8 different files.
We adopted two core principles to fix this:
- Single Responsibility Principle (SRP) — every function does exactly one thing
- API Chaining — dual-backend calls follow a clear fallback pipeline
This post explains the architecture, shows real code, and maps it to AP Computer Science Principles (APCSP) Big Ideas.
Architecture Overview
The 5-Layer Pattern
Every page in Hunger Heroes follows the same 5-layer structure:
┌───────────────────────────────────────┐
│ 1. ERROR CONFIG │ ← Constants, error messages
├───────────────────────────────────────┤
│ 2. API WORKERS │ ← One fetch = one function
├───────────────────────────────────────┤
│ 3. DATA TRANSFORMERS │ ← Normalize, compute, sort
├───────────────────────────────────────┤
│ 4. UI RENDERERS │ ← Build HTML, update DOM
├───────────────────────────────────────┤
│ 5. ORCHESTRATORS │ ← Wire layers together
└───────────────────────────────────────┘
Layer 1 — Error Config: Centralized error types and human-readable messages. No magic strings scattered in catch blocks.
Layer 2 — API Workers: Each worker does exactly one API call. postToSpring() posts to Spring. postToFlask() posts to Flask. That’s it. No UI, no branching, no side effects.
Layer 3 — Data Transformers: Pure functions. Input → computation → output. computeUrgency(donation) calculates time remaining and returns a typed urgency object. normalizeStatus(raw) cleans up backend-inconsistent status strings.
Layer 4 — UI Renderers: Take data, produce HTML. renderDonationCard(donation) builds a card. showToast(msg, type) shows a notification. They don’t fetch; they don’t decide.
Layer 5 — Orchestrators: The conductors. They call workers, pass results to transformers, then hand off to renderers. This is the only layer with control flow and branching logic.
Shared Module — donationApi.js
Instead of copy-pasting utility functions into each page, we created a single shared module:
assets/js/api/donationApi.js (~290 lines)
Every page imports what it needs:
import {
javaURI, pythonURI, fetchOptions,
springFetch, flaskFetch, dualFetch, dualPost,
computeUrgency, urgencyBadge, sortByUrgency,
normalizeStatus, statusBadge, sourceBadge,
normalizeDonationList, generateLocalId,
getLocalDonations, saveLocalDonation, cleanupLocalDonations,
showToast, errorPlaceholder, emptyPlaceholder,
CATEGORY_EMOJIS, ERROR_TYPES, ERROR_MESSAGES, getErrorMessage
} from '/hunger_heroes/assets/js/api/donationApi.js';
Key Exports Explained
| Export | Layer | Purpose |
|---|---|---|
springFetch(url, opts) |
Worker | 8s timeout, typed error handling |
flaskFetch(url, opts) |
Worker | 5s timeout, typed error handling |
dualFetch(springUrl, flaskUrl, opts, transformFlask) |
Worker | Spring → Flask fallback pipeline |
dualPost(endpoint, body) |
Worker | POST to both backends |
computeUrgency(donation) |
Transformer | Returns {level, label, emoji, color, cssClass, hoursLeft} |
urgencyBadge(donation) |
Transformer | HTML badge string |
sortByUrgency(donations) |
Transformer | Sort by expiration (most urgent first) |
normalizeStatus(raw) |
Transformer | Cleans inconsistent status strings |
statusBadge(raw) |
Renderer | Colored status HTML badge |
sourceBadge(source) |
Renderer | ☕ Java Spring / 🐍 Flask badge |
showToast(msg, type) |
Renderer | Auto-dismissing notification |
API Chaining — The Dual-Backend Pattern
The Problem
We have two independent backends:
- Spring Boot (primary) —
localhost:8286/hungerherosspring.opencodingsociety.com - Flask (fallback) —
localhost:8288/hungerheros.opencodingsociety.com
If Spring is down, we still want the app to work. If both are down, we fall back to localStorage.
The Solution — dualFetch()
// ── API Worker: Dual Fetch (Spring → Flask fallback) ──
async function dualFetch(springUrl, flaskUrl, opts = {}, transformFlask = d => d) {
try {
const data = await springFetch(springUrl, opts);
return { data, source: 'spring' };
} catch {
const data = await flaskFetch(flaskUrl, opts);
return { data: transformFlask(data), source: 'flask' };
}
}
Every page uses this same pattern. The orchestrator doesn’t know or care which backend answered:
// ── Orchestrator: Load Donations ──
async function loadDonations() {
try {
const { data, source } = await dualFetch(
`${javaURI}/api/donations`,
`${pythonURI}/api/donations`,
fetchOptions,
normalizeDonationList // transform Flask array format
);
const sorted = sortByUrgency(data); // Transformer
render(sorted, source); // Renderer
} catch (err) {
container.innerHTML = errorPlaceholder(getErrorMessage(err));
}
}
Three-Tier Fallback on Scan
The scan page has the most complex chain — Spring → Flask → localStorage:
User scans QR code
├── Try Spring: POST /api/donations/scan ── success → show result
├── Try Flask: POST /api/donations/scan ── success → show result
└── Try Local: localStorage.getItem() ── success → show result (offline mode)
Each tier is its own worker function (trySpringScan(), tryFlaskScan(), tryLocalStorage()), and the orchestrator (lookupDonation()) chains them with try/catch.
Urgency System
How It Works
Every donation has a prepared_at timestamp. The frontend computes urgency in real-time:
function computeUrgency(donation) {
const created = new Date(donation.prepared_at || donation.created_at || donation.timeCreated);
const hoursElapsed = (Date.now() - created.getTime()) / 3600000;
const shelfLife = donation.shelf_life_hours || 4;
const hoursLeft = Math.max(0, shelfLife - hoursElapsed);
if (hoursLeft <= 0) return { level: 'expired', emoji: '⚫', label: 'Expired', ... };
if (hoursLeft < 1) return { level: 'urgent', emoji: '🔴', label: 'Urgent', ... };
if (hoursLeft <= 3) return { level: 'expiring', emoji: '🟡', label: 'Expiring Soon', ... };
return { level: 'fresh', emoji: '🟢', label: 'Fresh', ... };
}
Visual Indicators
| Level | Emoji | Badge Color | Meaning |
|---|---|---|---|
| Fresh | 🟢 | Green | More than 3 hours left |
| Expiring Soon | 🟡 | Orange | 1–3 hours remaining |
| Urgent | 🔴 | Red | Less than 1 hour |
| Expired | ⚫ | Gray | Time’s up |
Urgency badges appear on:
- Browse page — every donation card
- Manage page — donor’s own donations
- Match page — search result cards
- Scan page — urgent warnings banner after scanning
Auto-Sorting
sortByUrgency(donations) places the most urgent items first, so volunteers see the items that need immediate pickup at the top.
SRP in Action — Real Examples
❌ Before (Everything in One Function)
async function loadAndRenderDonations() {
try {
let response = await fetch(javaURI + '/api/donations', fetchOptions);
if (!response.ok) {
response = await fetch(pythonURI + '/api/donations', fetchOptions);
}
const data = await response.json();
let html = '';
data.forEach(d => {
const status = d.status || d.donation_status || 'unknown';
const badge = status === 'posted' ? '<span class="badge green">Active</span>' : '...';
html += `<div class="card">${d.food_name} ${badge}</div>`;
});
document.getElementById('container').innerHTML = html;
} catch (err) {
document.getElementById('container').innerHTML = '<p>Error loading</p>';
}
}
Problems: Fetching + fallback + normalization + rendering + error handling all in one block. Impossible to test, reuse, or debug.
✅ After (SRP)
// ── Worker ──
// dualFetch() handles Spring → Flask fallback
// ── Transformer ──
// normalizeStatus() cleans status strings
// sortByUrgency() orders by expiration
// ── Renderer ──
function renderDonationCard(donation, source) {
return `
<div class="card">
${donation.food_name}
${urgencyBadge(donation)}
${statusBadge(donation.status)}
${sourceBadge(source)}
</div>`;
}
// ── Orchestrator ──
async function loadDonations() {
const { data, source } = await dualFetch(springUrl, flaskUrl, opts, normalizeDonationList);
const sorted = sortByUrgency(data);
container.innerHTML = sorted.map(d => renderDonationCard(d, source)).join('');
}
Result: Each function can be tested, reused, and debugged independently.
APCSP Big Ideas Mapping
Big Idea 1 — Creative Development
- CRD-2.B: We developed the urgency system iteratively — first a simple timer, then a 4-tier color system with auto-sorting
- CRD-2.G: The shared
donationApi.jsmodule was created to eliminate code duplication across 8 pages
Big Idea 2 — Data
- DAT-2.A: Donations are stored as structured objects with typed fields (
food_name,category,status,prepared_at) - DAT-2.E:
computeUrgency()transforms raw timestamps into meaningful urgency classifications
Big Idea 3 — Algorithms and Programming
- AAP-2.A: Every function follows SRP —
fetchSpringMatches()only fetches,renderMatchCard()only renders - AAP-2.M: The dual-fetch pattern uses algorithmic fallback: try primary → catch → try secondary → catch → local fallback
- AAP-3.A:
dualFetch()is a procedural abstraction — 8 pages call it without knowing the internal retry logic
Big Idea 4 — Computing Systems and Networks
- CSN-1.B: The app works across two independent backend servers with graceful degradation
- CSN-1.D: API calls use CORS, credentials, timeouts, and error classification for robust networking
Big Idea 5 — Impact of Computing
- IOC-1.A: The platform addresses food waste and hunger — computing used for social good
- IOC-1.B: The urgency system prioritizes perishable food, reducing waste in real-time
Pages Refactored
| Page | File | Key SRP Functions | Lines Saved |
|---|---|---|---|
| Create | create.md |
collectDonationData(), postToSpring(), postToFlask() |
~40 |
| Scan | scan.md |
trySpringScan(), tryFlaskScan(), tryLocalStorage(), buildUrgencyWarnings() |
~60 |
| Browse | browse.md |
renderBrowseCard(), filterByStatus(), sortDonations() |
~50 |
| Manage | manage.md |
renderDonationCard(), executeDonationAction(), filterManage() |
~50 |
| Match | match.md |
fetchSpringMatches(), fetchFlaskMatches(), renderMatchCard() |
~45 |
| Leaderboard | leaderboard.md |
fetchStats(), fetchLeaderboard(), animateCounter() |
~35 |
| Network | network.md |
fetchSpringGraph(), buildFlaskGraph(), renderCommunities() |
~40 |
| Categories | categories.md |
fetchSpringTree(), buildFlaskTree(), renderTree() |
~35 |
Total: ~355 lines of duplicated code eliminated via shared donationApi.js module.
Conclusion
The SRP + API Chaining refactor transformed Hunger Heroes from a working prototype into a maintainable, testable architecture. Every function has one job, every API call has a fallback, and every user sees urgency indicators that help save food before it expires.
The shared module pattern means new pages can be added in minutes — just import the workers and transformers you need, write page-specific renderers, and connect them with an orchestrator.