Jak vytvořit vlastní cache server pro různá API?

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í.