Only show guides for your app
Fetch a public API, cache the result with a TTL map so you don't hammer the upstream, and serve it as your own JSON endpoint — the full Node.js build with run steps.
Weather, crypto prices, GitHub stats — plenty of useful data lives behind public APIs. But calling them on every page load is slow and can get you rate-limited or blocked. This guide builds a small aggregator: it fetches an upstream API once, caches the result for a while, and serves it as your own fast JSON endpoint. It runs on the Node.js application and uses fetch (built into Node) plus a tiny time-to-live cache.
| At a glance | |
|---|---|
| You need | A server running the Node.js application |
| Build with | Express + Node's built-in fetch + a TTL cache |
| Plan | Any — free runs while your session timer has time, premium runs 24/7 |
| Time | About twenty minutes |
New to Node here? Node.js on Falix covers the entry file, auto-install, and SERVER_PORT.
browser → your /api/data → (cache fresh?) → yes → serve cached copy
→ no → fetch upstream, store, serve
The cache is the whole point. Set a TTL (time to live) of, say, 60 seconds and the upstream is called at most once a minute no matter how many visitors you have. Fewer calls means faster responses and no rate-limit trouble.
package.json:
{
"name": "api-aggregator",
"version": "1.0.0",
"private": true,
"dependencies": {
"express": "^4.19.2"
}
}
index.js — the whole aggregator:
const express = require('express');
const path = require('node:path');
const app = express();
app.use(express.static(path.join(__dirname, 'public')));
// The upstream API we aggregate. This one needs no auth key.
// Swap in any public JSON API; if it needs a key, read it from process.env.
const UPSTREAM = process.env.UPSTREAM_URL || 'https://api.coindesk.com/v1/bpi/currentprice.json';
// --- tiny cache: value + expiry timestamp, keyed by request ---
const cache = new Map();
const TTL_MS = 60 * 1000; // 60 seconds
async function getCached(key, fetcher) {
const hit = cache.get(key);
if (hit && hit.expires > Date.now()) {
return { data: hit.data, cached: true };
}
const data = await fetcher();
cache.set(key, { data, expires: Date.now() + TTL_MS });
return { data, cached: false };
}
app.get('/api/data', async (req, res) => {
try {
const { data, cached } = await getCached('data', async () => {
const r = await fetch(UPSTREAM);
if (!r.ok) throw new Error(`upstream ${r.status}`);
return r.json();
});
res.set('X-Cache', cached ? 'HIT' : 'MISS');
res.json(data);
} catch (err) {
console.error('aggregate failed:', err.message);
res.status(502).json({ error: 'Upstream unavailable.' });
}
});
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
Press Start. npm install runs (Express only — fetch is built into Node 18+), then Listening on port …. Open your address and hit /api/data.
The response carries an X-Cache header: MISS the first time (it went upstream) and HIT for the next 60 seconds (served from memory). Refresh a few times and watch it flip.
🎯 Good to know about the verification: this build's caching was checked against a local mock API running beside it — the mock counted its own hits, which proved the upstream is called on the first request and not called again while the cache is fresh (even when the mock was stopped, the cached copy still served). The
fetchcall itself is standard; pointUPSTREAMat any real public JSON API and it behaves the same way.
cache is a plain Map. Each entry stores { data, expires }:
| Step | What happens |
|---|---|
| Request comes in | Look up the key in the map |
Entry exists and expires > now |
Return it, mark cached: true — no network call |
| Missing or expired | Call fetcher(), store { data, expires: now + TTL }, return cached: false |
That's a complete TTL cache in a dozen lines. No library needed.
💡 Tip: Cache keys matter. This example uses one fixed key (
'data') because it serves one upstream. If you aggregate per city or per user, build the key from the request —getCached(\weather:${city}`, ...)` — so each variant caches separately.
UPSTREAM to the URL. Many public APIs (open data, some crypto and weather feeds) work with no auth..env (API_KEY=...), load it with dotenv, and add it to the request — as a header or query string per the API's docs. See Environment variables & secrets.await Promise.all([...]) inside the fetcher and return one merged object. The cache stores the merged result, so all of them refresh on the same TTL.index.html in public/ that fetches /api/data and renders it — your server becomes both the page and its API, like the Express website + API build.cache.delete('data') so you can force a fresh pull on demand.TTL_MS to minutes or hours for things that rarely change.X-Cache always says MISS — your TTL is too short, or you're restarting between requests (an in-memory cache resets on restart — that's expected).listen call must use SERVER_PORT and 0.0.0.0. See I can't reach my app.⚠️ Heads up: This cache lives in memory, so it clears on every restart and isn't shared across instances. For a cache that survives restarts or is shared, use Redis caching — the same TTL idea, backed by a real store.