Choose the data this fits
A public store directory can start as a static JSON file when visitors read the same data. Keeping the downloaded list in browser memory avoids another database lookup for each keyword change. Private user data or live inventory needs a different design.
Static files still need hosting and an update process. This article explains the choice and a search example; it makes no claim about cost savings or response-time improvements.
Trace the local BAKUO flow
In the local BAKUO demo, scripts/build-dataset.mjs writes processed data to public/data/exchanges.json. src/api/exchangeService.ts fetches the configured data URL, checks the HTTP status, then filters the list. Tracing those steps separates data publication from browser use.
The same project also requests exchange-rate data separately. Static store listings therefore do not mean “no external APIs” across the service. The repository has both demo and frontend; which implementation matches the current deployment needs a separate check.
Check size and update conditions
Field counts, text lengths, and language duplicates affect the download. Check file bytes and actual HTTP transfer size separately, then observe time to the first usable list on a phone. Script execution or marker creation can block the screen even with a small file.
The source reference date can differ from the deployment date. Preserve the publisher’s date and check missing fields and duplicates on updates. A static file is visible to anyone downloading it, so include only publishable fields.
Search a small directory
Run this complete example in a browser console or Node.js. Its two entries are synthetic, not real businesses. It makes no HTTP requests; it checks data validation and in-memory searching.
function prepareDirectory(input) {
if (!Array.isArray(input)) throw new TypeError('Directory must be an array');
return input.map((record, index) => {
if (!record || typeof record.id !== 'string'
|| typeof record.name !== 'string' || typeof record.address !== 'string'
|| !record.id.trim() || !record.name.trim() || !record.address.trim()) {
throw new TypeError('Invalid directory record at index ' + index);
}
return {
...record,
searchText: (record.name + ' ' + record.address).normalize('NFC').toLowerCase()
};
});
}
function searchDirectory(records, query) {
if (typeof query !== 'string') throw new TypeError('Query must be a string');
const needle = query.normalize('NFC').trim().toLowerCase();
return records.filter(record => record.searchText.includes(needle));
}
// Synthetic directory entries for this example, not real businesses.
const records = prepareDirectory([
{ id: 'A', name: 'Harbor Exchange', address: 'Example Road' },
{ id: 'B', name: 'Garden Exchange', address: 'Sample Street' }
]);
console.log(searchDirectory(records, 'HARBOR').map(record => record.id)); // [ 'A' ]
console.log(searchDirectory(records, '').length); // 2
console.log(searchDirectory(records, 'missing').length); // 0The outputs are ["A"], 2, and 0. An empty query means the full list, while an unmatched query is a valid empty result. Malformed data throws instead of being silently replaced with an empty successful response.
Design publication and failure states
Distinguish loading, a usable list, no search matches, and a download failure in the actual interface. A retry button must make another real request. Do not substitute invented stores or silently present old data as current. If cached data is intentionally shown, disclose its date and why it is being used.
After publishing, check that stale HTML and new data do not form an incompatible combination. Plan filenames, cache policy, and the update process together, then inspect real HTTP headers. Continue with checking this site’s cache configuration for policies by file role.