Skip to main content

API Call Flow

Complete documentation of how API calls flow through the AquaGen application, from user interaction to UI update.


Overview

AquaGen follows a unidirectional data flow pattern:


Complete API Flow Diagram

1. GET Request Flow (Fetching Data)


2. POST Request Flow (Creating/Updating Data)


Code Examples

These examples are illustrative

The snippets below show the pattern, not verbatim source. Real stores are functional (React Context + hooks), controllers/dataSources are plain modules (not classes), and file names vary per feature. Treat the code as a shape to follow, and check the actual files for exact APIs.

Example 1: GET Request (Fetching Dashboard Data)

Component Layer

// libs/dashboard/src/DashboardPage.jsx
import { useContext, useEffect } from 'react';
import { DashboardContext } from './store/DashboardStore';

function DashboardPage() {
const dashboardStore = useContext(DashboardContext);

useEffect(() => {
// Fetch data on component mount
dashboardStore.fetchDashboardData();
}, []);

if (dashboardStore.loading) {
return <Loader />;
}

if (dashboardStore.error) {
return <ErrorMessage error={dashboardStore.error} />;
}

return (
<div>
<TotalConsumption value={dashboardStore.data.total} />
<AlertsSummary alerts={dashboardStore.data.alerts} />
</div>
);
}

Store Layer

// libs/dashboard/src/store/DashboardStore.js
import { createContext, useState } from 'react';
import { DashboardController } from '../controller/DashboardController';

export const DashboardContext = createContext();

export function DashboardProvider({ children }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);

const fetchDashboardData = async () => {
setLoading(true);
setError(null);

try {
const result = await DashboardController.fetchData();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

const value = {
data,
loading,
error,
fetchDashboardData
};

return (
<DashboardContext.Provider value={value}>
{children}
</DashboardContext.Provider>
);
}

Controller Layer

// libs/dashboard/src/controller/DashboardController.js
import { DashboardDataSource } from '../dataSource/DashboardDataSource';

class DashboardController {
async fetchData() {
// 1. Get raw data
const rawData = await DashboardDataSource.getDashboardData();

// 2. Transform data for UI
const transformedData = {
total: rawData.totalConsumption || 0,
alerts: rawData.activeAlerts || [],
quality: rawData.waterQuality || 'Unknown',
chartData: this.formatChartData(rawData.chartData)
};

// 3. Apply business rules
if (transformedData.total < 0) {
throw new Error('Invalid consumption data');
}

return transformedData;
}

formatChartData(data) {
return data.map(item => ({
date: new Date(item.timestamp),
value: parseFloat(item.value),
label: item.label
}));
}
}

export default new DashboardController();

DataSource Layer

// libs/dashboard/src/dataSource/DashboardDataSource.js
import { apiClient } from '@aquagen-mf-webapp/shared/services';

class DashboardDataSource {
async getDashboardData() {
// apiClient.get(url, params, headers, customConfig) — params is the 2nd arg
const response = await apiClient.get('/landingPage/userData', {
startDate: '2024-01-01',
endDate: '2024-01-31',
});

return response.data;
}
}

export default new DashboardDataSource();

Example 2: POST Request (Manual Entry)

Component Layer

// Illustrative — a manual-entry form component
// (real manual-node UI lives under libs/manageAccount/src/components/manualNodeEntry/)
import { useContext, useState } from 'react';
import { ManualEntryContext } from '../store/ManualEntryStore';

function ManualEntryForm() {
const [formData, setFormData] = useState({
nodeId: '',
value: '',
timestamp: new Date()
});

const manualEntryStore = useContext(ManualEntryContext);

const handleSubmit = async (e) => {
e.preventDefault();

try {
await manualEntryStore.submitEntry(formData);

// Show success message
alert('Entry saved successfully!');

// Reset form
setFormData({
nodeId: '',
value: '',
timestamp: new Date()
});
} catch (error) {
alert('Error: ' + error.message);
}
};

return (
<form onSubmit={handleSubmit}>
<input
value={formData.nodeId}
onChange={(e) => setFormData({...formData, nodeId: e.target.value})}
placeholder="Node ID"
/>
<input
type="number"
value={formData.value}
onChange={(e) => setFormData({...formData, value: e.target.value})}
placeholder="Value"
/>
<button type="submit" disabled={manualEntryStore.submitting}>
{manualEntryStore.submitting ? 'Saving...' : 'Save Entry'}
</button>
</form>
);
}

Store Layer

// Illustrative — a feature store (real: libs/manageAccount/src/store/ManualNodeStore.js)
import { createContext, useState } from 'react';
import { ManualEntryController } from '../controller/ManualEntryController';

export const ManualEntryContext = createContext();

export function ManualEntryProvider({ children }) {
const [entries, setEntries] = useState([]);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState(null);

const submitEntry = async (data) => {
setSubmitting(true);
setError(null);

try {
const result = await ManualEntryController.createEntry(data);

// Add to local state
setEntries([...entries, result]);

return result;
} catch (err) {
setError(err.message);
throw err;
} finally {
setSubmitting(false);
}
};

const value = {
entries,
submitting,
error,
submitEntry
};

return (
<ManualEntryContext.Provider value={value}>
{children}
</ManualEntryContext.Provider>
);
}

Controller Layer

// Illustrative — a feature controller (real: libs/manageAccount/src/controller/manualNodeController.js)
import { ManualEntryDataSource } from '../dataSource/ManualEntryDataSource';

class ManualEntryController {
async createEntry(data) {
// Validate
if (!data.nodeId || !data.value) {
throw new Error('Node ID and value are required');
}

// Format for API
const formattedData = {
node_id: data.nodeId,
measurement_value: parseFloat(data.value),
timestamp: data.timestamp.toISOString(),
unit: 'liters',
source: 'manual_entry'
};

// Send to backend
const response = await ManualEntryDataSource.postEntry(formattedData);

// Transform response
return {
id: response.id,
nodeId: response.node_id,
value: response.measurement_value,
timestamp: new Date(response.timestamp),
createdAt: new Date(response.created_at)
};
}
}

export default new ManualEntryController();

DataSource Layer

// Illustrative — a feature dataSource (real: libs/manageAccount/src/dataSource/account.js)
import { apiClient } from '@aquagen-mf-webapp/shared/services';

class ManualEntryDataSource {
async postEntry(data) {
const response = await apiClient.post('/virtual/device_data', data);
return response.data;
}
}

export default new ManualEntryDataSource();

API Client Configuration

Request Interceptor

// libs/shared/src/services/api/apiClient.js
import { browserName, browserVersion, deviceType } from 'react-device-detect';

axiosClient.interceptors.request.use((config) => {
// Token comes from LocalDB (localStorage-backed), NOT sessionStorage
const loginResponse = JSON.parse(LocalDBInstance.getItem(LocalDBKeys.loginResponse));

// Raw JWT — there is NO "Bearer " prefix. Refresh calls send the refreshToken instead.
config.headers['Authorization'] =
config.url === Urls.refreshToken
? loginResponse?.refreshToken
: loginResponse?.token;

// Device headers — note the exact names (none are x-prefixed)
config.headers['browserName'] = browserName;
config.headers['browserVersion'] = browserVersion;
config.headers['deviceType'] = deviceType;
config.headers['operationSystem'] = getOS();
config.headers['deviceId'] = getDeviceId();

return config;
});

Response Interceptor

// The real interceptor handles only two status codes: 440 and 420.
axiosClient.interceptors.response.use(
(res) => res, // success — return the response (callers read res.data)
async (err) => {
const status = err?.response?.status;

if (status === 440) {
// Session/token invalid — clear login and send the user to /login
LocalDBInstance.removeItem(LocalDBKeys.loginResponse);
LocalDBInstance.removeItem(LocalDBKeys.loginType);
window.location.pathname = '/login';
} else if (status === 420) {
// Plan/subscription gate — send the user home
window.location.pathname = '/';
} else if (err.response?.data) {
throw err.response.data; // hand the error body to the caller's catch
} else {
throw err;
}
}
);

Error Handling Flow


Loading States Flow


Real-Time Updates (WebSocket)


Best Practices

1. Always Use Try-Catch

const fetchData = async () => {
try {
const data = await apiClient.get('/endpoint');
setData(data);
} catch (error) {
setError(error.message);
}
};

2. Show Loading States

if (loading) return <Loader />;
if (error) return <ErrorMessage error={error} />;
return <DataDisplay data={data} />;

3. Validate Before Sending

// In Controller
if (!this.validateData(data)) {
throw new Error('Invalid data format');
}

4. Transform Data in Controller

// Controller transforms API response to UI format
const transformedData = {
displayDate: formatDate(rawData.timestamp),
displayValue: formatNumber(rawData.value),
...
};

5. Keep DataSource Thin

// DataSource only handles API calls
async getData() {
return await apiClient.get('/endpoint');
}

Next Steps