Jak vytvořit vlastní cache server pro různá API?
Pokud chceme na svém webu nebo nějakém jiném zobrazovat příspěvky ze sociálních sítí, či pracovat s nějakou API, je často dobré mít vlastní server, který výsledky z původní API nacachuje. To platí zejména ve chvíli, kdy jsou dotazy na API limitované a hrozilo by reálné riziko, že se jednoho dne server dané služby zapře a už nic nepošle.
V tomto článku bych se chtěl věnovat tomu, jak si takovou jednoduchou API cache vytvořit s pomocí Express.js aplikace. Předpokládám tedy alespoň základní znalost JavaScriptu, práce s NPM (Node Package Manager) a příkazovou řádkou.
Začínáme
Nejjednodušším způsobem, jak začít s Express.js aplikací je využít tzv. Express application generator. To je
jednoduchý nástroj, který přípraví základní strukturu projektu a do souboru package.json
přidá informaci o
knihovnách, které budeme pro naši aplikaci potřebovat. Začněme tedy vytvořením složky s naší aplikací:
1mkdir API-Cache && cd API-Cache
Následně ve složce vytvoříme novou Express.js aplikaci a doinstalujeme několik knihoven. Doporučuji se také podívat na případné možnosti konfigurace v dokumentaci.
1npx express-generator && npm install
V naší složce by se tak měla vytvořit základní struktura projektu včetně složky node_modules
obsahující všechny knihovny
specifikované v souboru package.json
. Pro náš projekt budeme ovšem potřebovat ještě několik dalších knihoven, které
generátor neobsahuje, a to dotenv, cors a got, případně také nodemon pro automatické restartování serveru při změně v kódu.
1npm install --save dotenv cors got 2# jelikož v produkci nodemon nepotřebujeme, nainstalujeme jej pouze jako dev dependency 3npm install --save-dev nodemon
Nakonec vytvoříme prázdný .env
soubor, který bude obsahovat citlivá data naší aplikace, například autentizační tokeny.
1touch .env
Tímto bychom měli mít připraveno vše potřebné pro vývoj aplikace.
Zdroj informací
Pro naši příkladovou aplikaci použijeme jako zdroj informací volně přístupnout API Quotes on Desing, která obsahuje řadu citátů významných osobností. Informace k API jsou naleznutelné zde pro naše účely si zkopírujte tento odkaz:
https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand&per_page=1
Výsledky jsou řazené náhodně, navíc díky parametru per_page=1
se nám zobrazí vždy pouze jeden citát.
Cache
Dále potřebujeme vyřešit cache providera, v tomto článku budu pracovat s cache v paměti serveru. Nejedná se o zcela ideální řešení, zejména pokud je potřeba uchovat větší množství dat nebo by měla data v cache zůstat i po restartu serveru. V takovém případě by bylo lepší zvolit cache, která se ukládá na disk. Nicméně v tomto případě předpokládám, že data zůstanou v cache jen maximálně několik desítek minut.
Vytvoříme si tedy jednoduchou constructor function/class, která bude držet naše data. Vytvoříme si tedy složku mkdir utils
, ve které budeme
mít uloženou naši constructor function/class - DataCache.js
.
DataCache.js
1function DataCache (fetchFunction, minutesToLive = 10) { 2 this.fetchFunction = fetchFunction; 3 this.millisecondsToLive = minutesToLive * 60 * 1000; 4 this.fetchDate = new Date(0); 5 this.cache = null; 6 7 //... 8 9} 10 11module.exports = DataCache;
Základní podoba naší constructor function/class DataCache je tedy následující, přijímá dva parametry.
První fetchFunction
je metoda, která má načíst data z našeho zdroje, pokud je potřeba aktualizovat cache.
Druhý parametr je doba životnosti dat v cache než přestanou být validní. Pro lepší přehlednost je budeme zadávat v minutách
a přepočteme je až následně do miliseknud. Dále si připravíme proměnnou this.cache
, jež by měla obsahovat data a this.fetchDate
,
která bude nést datum posledního načtení dat do cache.
Do naší třídy si také přidáme funkci isCacheExpired()
abychom zjistili, zdali už cache expirovala, či nikoliv.
1 this.isCacheExpired = () => { 2 return (this.fetchDate.getTime() + this.millisecondsToLive) < new Date().getTime(); 3 }
Metoda porovnává momentální čas se součtem času uloženém v proměnné
fetchDate
a životností cache. Pokud je tento čas menší než je momentální čas, cache expirovala.
Nejdůležitější funkcí je ovšem getData()
, neboť zajišťuje jednak načtení dat ze zdroje a jednak jejich vrácení z cache, a to
v závislosti na tom, zdali jsou v cache uložené a je stále validní.
1 this.getData = () => { 2 if (!this.cache || this.isCacheExpired()) { 3 console.log('expirováno - načítám nová data'); 4 return this.fetchFunction() 5 .then((data) => { 6 this.cache = data; 7 this.fetchDate = new Date(); 8 return data; 9 }); 10 } else { 11 console.log('data jsou v cache'); 12 return Promise.resolve(this.cache); 13 } 14 }
Metoda vždy vrací Promise s daty. Pro lepší přehlednost jsem také v kódu nechal logy, jež nám v příkazovém řádku ukáží, jestli server načítal data nově nebo z cache. Tímto máme hotovou naši DataCache třídu a můžeme ji vyzkoušet v praxi.
Načítání dat na serveru
Vytvoříme si tedy v rootu projektu složku s controllery mkdir controllers && cd controllers
a v ní controller touch QuoteController.js
. QuoteController bude obsahovat pouze jednu metodu, a to getQuote
, která se dotáže Quote on Design serveru na aktuální citát.
QuoteController.js
1const got = require('got'); 2const DataCache = require('../utils/DataCache'); 3 4function getQuote () { 5 return got(`https://quotesondesign.com/wp-json/wp/v2/posts/?orderby=rand&per_page=1`) 6 .then((response) => { 7 return JSON.parse(response.body); 8 }) 9 .catch((error) => { 10 return error; 11 }); 12} 13 14const quote = new DataCache(getQuote, 1); 15 16module.exports = quote;
Nejprve načteme potřebné knihovny, což je got, který budeme potřebovat pro
dotaz do API a naše třída DataCache. Následně
deklarujeme funkci getQuote()
která se skládá pouze z dotazu do API. Pro dotaz
použijeme knihovnu got, byť by šlo
využít i standardního HTTP modulu, nicméně got nám podstatně
ušetří práci. V then
bloku převedeme ještě tělo odpovědi do formátu JSON,
neboť got sám o sobě s odpovědí nijak nepracuje a vrací pouze textový řetězec.
Nakonec vytvoříme proměnnou, v níž inicializujeme naši DataCache třídu a vyexportujeme ji.
Do souboru ./routes/index.js
přidáme následující řádky. Require na náš controller přidáme pod ostatní
require řádky. Registraci naší cesty potom kamkoliv pod původní kód, avšak před module.exports
.
1// .. původní require statements 2const quoteController = require('../controllers/quoteController'); 3 4// ... původní kód 5router.get('/quote', async (req, res, next) => { 6 quoteController.getData().then((data) => { 7 res.json(data); 8 }); 9});
V registraci route pouze zavoláme controller a v něm metodu z naší třídy, jež pro nás zprostředkovává
data buďto z cache nebo z dotazu do API. Vrácená data potom odešleme ve formátu json. To je vše! Teď už
jen stačí spustit příkaz npm run start
a navštívit v prohlížeči stránku http://localhost:3000/quote
, kde by
se měl zobrazit náhodný citát! V konzoli serveru by mělo být vidět následující:
[nodemon] starting `node ./bin/www`
expirováno - načítám nová data
GET /quote 200 164.028 ms - 5056
data jsou v cache
GET /quote 200 1.710 ms - 5056
data jsou v cache
GET /quote 200 0.969 ms - 5056
Závěrem
Gratuluji k úspěšnému vytvoření velice jednoduché a základní API cache! Pokud jste se rozhodli, že vám
takováto jednoduchá cache stačí. Doporučuji podívat se na celý kód znovu a rozšířit, případně upravit zachytávání chyb, stejně tak
je asi vhodné odstranit z kódu console.log()
části. DataCache
třídu lze také velmi snadno rozšířit modulem fs
o možnost ukládání cache na disk, byť ani to nemusí být v některých případech ideální.