JavaScript na serveru: implementace REST API

node.js logo

Minulý díl byl věnován nejlepším postupům v tvorbě REST API, které v dnešním díle uvedeme do praxe. Kromě toho se podíváme podrobněji na kešování a zpracování chyb ve frameworku Express. Na konci článku si představíme známý český projekt Apiary, který při práci s API výrazně pomáhá.

Seriál: Node.js - s JavaScriptem na serveru (13 dílů)

  1. JavaScript na serveru: Začínáme s Node.js 23.11.2010
  2. JavaScript na serveru: Patří budoucnost Node.js? 21.9.2012
  3. JavaScript na serveru: Architektura a první Hello World 5.10.2012
  4. JavaScript na serveru: moduly a npm 12.10.2012
  5. JavaScript na serveru: začínáme programovat e-shop 19.10.2012
  6. JavaScript na serveru: MongoDB, Mongoose a AngularJS 26.10.2012
  7. JavaScript na serveru: Testování a kontinuální integrace 2.11.2012
  8. JavaScript na serveru: REST API 9.11.2012
  9. JavaScript na serveru: implementace REST API 16.11.2012
  10. JavaScript na serveru: nástroje a dokumentace 23.11.2012
  11. Začínáme s AngularJS 30.11.2012
  12. AngularJS direktivy a testování 7.12.2012
  13. JavaScript na serveru: CoffeeScript a šablonovací systémy 14.12.2012

Zdrojové kódy dnešního dílu jsou dostupné v tagu dil9, stáhnete si je jako obvykle příkazem  git checkout -f dil9.

Úprava REST API

Začněme několika úpravami aplikace podle teorie v minulém článku.

Chyba HTTP 406

Pokud uživatel zasílá v hlavičce Accept požadavek na formát, který nepodporujeme, měli bychom vrátit chybu HTTP 406. Nejsnáze se bude ošetření implementovat přes vlastní middleware:

module.exports = function() {
  return function(req, res, next){
    if (!req.accepts('json')) {
        return next(new NotAcceptable());
    }
    next();
  };
};

Objekt požadavku má k dispozici metodu accepts(), která ověřuje, zda je předaný řetězec obsažen v hodnotě hlavičky. Zde říkáme, že pokud uživatel neakceptuje odpovědi ve formátu JSON, vrátíme mu HTTP kód 406 implementovaný v NotAcceptable (viz dále). Metoda vrací formát z hodnoty hlavičky, který odpovídá zadanému řetězci, takže pokud uživatel v hlavičce Accept zašle mimo jiné hodnotu “application/json”, metoda vrátí právě tento řetězec.

Chyba HTTP 415

Chybový kód HTTP 415 je odeslán, pokud uživatel odeslal data s jinou hodnotou Content-Type, než dokážeme zpracovat. I zde použijeme pro ošetření middleware, které může vypadat takto:

module.exports = function() {
  return function(req, res, next){
    var isPostOrPut = req.method === 'POST' || req.method === 'PUT';
    var isBody = typeof req.body !== 'undefined';
    if (isPostOrPut && isBody && !req.is('json')) {
      return next(new UnsupportedMediaType());
    }
    next();
  };
};

Zde se používá metoda is(), která testuje hodnotu v hlavičce Content-Type. My budeme zpracovávat jen JSON, takže pokud uživatel zašle data s jinou hodnotou Content-Type, vrátíme mu HTTP chybu 415.

I když se může zdát, že ošetření na obě chyby je zbytečné, můžeme tím předejít nepříjemné situaci, kdy se bude uživatel snažit v dobré míře komunikovat s naším API v jiném formátu, než podporujeme. Pokud bychom situaci neošetřili, odpovídali bychom mu třeba oznámením, že chybí hodnota nějakého pole, nebo nějakou validační chybou, a asi si dokážete představit, že by mohlo trvat poměrně dlouho zjistit, proč je zpracování požadavku neúspěšné, když jsou prokazatelně všechna data zasílaná na API v pořádku.

Vložení stránky

Metoda pro vložení nové stránky bude nově odesílat při úspěšném provedení HTTP kód 201 a do hlavičky Location přidá plnou cestu k nové vytvořenému dokumentu přes metodu res.setHeader() a nově vytvořený utility modul s metodou  fullUrl():

exports.create = function(req, res, next){
  var page = new Page();
  page.title = req.body.title;
  page.url = url(req.body.title);
  page.content = req.body.content;
  page.save(function(err, doc) {
    if (err) return next(err);
    var location = util.fullUrl('/' + req.path + '/' + doc.url, req);
    res.setHeader('location', location);
    res.send(201);
  });
};

Smazání stránky

Úprava API u mazání stránky bude triviální. Pouze místo HTTP kódu 200 pošleme kód 204, který říká, že operace proběhla úspěšně a žádný další obsah není v těle odpovědi zaslán.

exports.destroy = function(req, res, next){
  req.page.remove(function(err, doc) {
    if (err) return next(err);
    res.send(204);
  });
};

Samozřejmě pro všechny middleware se přidají adekvátní testy a upraví se ty stávající. Všechny testy jsou jako obvykle v repozitáři na Githubu.

Kešování

Kešování obsahu výrazně nabylo na důležitosti s příchodem smartphonů a mobilního webu, kde je díky FUP potřeba zvažovat každý přenesený bajt. Pokud je pro vás pojem kešování pojmem neznámým, můžete si prostudovat Kešovací návod od Dušana Janovského.

Pro nás jsou důležité především dvě dvojce hlaviček:

  1. Last-Modified a If-Modified-Since a
  2. ETag a If-None-Match

Jako hodnotu hlavičky Last-Modified odesílá server datum poslední změny dokumentu. Při dalším HTTP požadavku pak prohlížeč zašle datum v hlavičce If-Modified-Since a server může zkontrolovat, zda se dokument od té doby změnil. Pokud se nezměnil, odešle odpověď jen s HTTP kódem 304 bez dalších dat, takže uživatel nemusí opětovně stahovat množství dat. Podobně funguje i druhá kombinace ETag a If-None-Match, jen s tím rozdílem, že se neodesílá datum, ale hash, který je unikátní pro daný stav dokumentu.

Framework Express má zabudovanou automatickou podporu pro kešování. Pokud jsou klientovi odeslána data větší než 1KB, před odesláním dat framework vygeneruje unikátní hash a ten odešle v hlavičce ETag. Než ovšem data odešle, bude v požadavku hledat hlavičku If-None-Match a porovná, zda se hodnoty hashů rovnají. Pokud ano, nebude odesílat žádný obsah v těle požadavku a odešle pouze HTTP kód 304.

Ačkoliv framework odesílá ETag automaticky, pokud jsme schopni sledovat datum změny dokumentu, je dobré odeslat i hlavičku Last-Modified. Automatické odesílání ETag sice zabrání tomu, že uživatel nebude stahovat data zbytečně, ale nesníží zátěž serveru při zpracování požadavku. Pokud hned na začátku zpracování HTTP požadavku dokážeme podle zaslaného data říct, zda má prohlížeč čerstvá data, můžeme ihned ukončit zpracování požadavku odesláním HTTP kódu 304.

Connect middleware

O middleware jsme se již bavili již dříve, proto jen zopakuji, že jsou to funkce, které jsou vykonány pro HTTP každý požadavek. Jejich přehled je k dispozici na stránkách projektu. Vedle toho existuje celá řada komunitních middleware.

Middleware mohou být dvojího druhu a liší se podle počtu parametrů. Mají-li 3 a méně parametrů, pak jsou určeny pro běžný HTTP požadavek. Mají-li však přesně 4 parametry, jsou určeny pro zpracování chyb.

Příklad běžného middleware jsme si ukázali výše u http406 či http415. Jsou zde tři parametry. První reprezentuje požadavek, druhý odpověď a třetí funkci next(), která přesune zpracování na další běžný middleware, ovšem pouze pokud neobsahuje žádný parametr. Pokud nějaký parametr předán je, bude další zpracování přesměrováno na middleware pro zpracování chyb a jako první parametr bude právě to, co jsme předali jako hodnotu parametru funkci next().

Do našeho e-shopu jsem přidal jeden jednoduchý middleware favicon. Ten zkontroluje, zda požadavek nepřichází na URL /favicon.ico, na což se ptají prohlížeče. Pokud ano, odešle prohlížeči obsah souboru favicon.ico a přidá hlavičky pro kešování.

Zpracování chyb

Vytvoříme si jeden modul error, kde budeme vytvářet reprezentaci každé chyby, která vznikne v controllerech. Takže např. zpracování chyby pro HTTP kód 406 může vypadat takto:

var util = require('util');
function NotAcceptable(message) {
  message = message || 'Pozadavek na format, ktery neni podporovan.';
  AppError.call(this, message, 406);
}
util.inherits(NotAcceptable, AppError);

Třída (resp. konstrukční funkce) nastavuje pouze atributy status (reprezentuje HTTP status),  name (pojmenování chyby, aby ji bylo možné dohledat v dokumentaci) a message (jednoduchý popis chyby).

V controlleru pak vytvoříme objekt NotAcceptable, který předáme funkci next(), jak bylo ukázáno na začátku článku.

Povšimněte si posledního řádku, kde se používá funkce inherits() z modulu util. Zde poprvé používáme modul, který je přímo součástí Node.js. Obsahuje několik užitečných funkcí, které se používají velmi často. Funkce inherits() slouží pro vytvoření dědičnosti. Říkáme zde, že třída NotAcceptable je potomkem třídy AppError, což je výchozí třída pro všechny třídy reprezentující nějakou chybu.

Metoda util.inherits() zaručí propojení prototypů potomka na předka, to znamená že potomek získá všechny metody předka. Konstruktor předka se ale nevolá automaticky, a proto je přidáno jeho volání jako poslední řádek do naší nové třídy NotAcceptable. Toto je velice důležité, pokud má dědění v JavaScriptu fungovat tak, jak byste očekávali z jiných objektových jazyků vycházejících z C syntaxe. Podobným „neduhem“ jako JavaScript trpí např. Python.

Nakonec již zbývá přidat middleware pro zpracování chyb. Jak bylo řečeno dříve, musí mít 4 parametry, aby ho Express bral jako middleware pro zpracování chyb. Může vypadat např. takto:

module.exports = function() {
  return function(err, req, res, next){
    if (err instanceof AppError) {
      return res.send(err.status, {
        type: err.type,
        message: err.message
      });
    }

    if (err instanceof Error) {
      if (err.name === 'ValidationError') {
        return res.send(400, {
          type: 'ValidationError',
          message: err.message,
          errors: err.errors
        });
      }
    }

    next(err);
  };
};

Zde rozlišujeme tři druhy chyb. První jsou potomky AppError. Druhé jsou potomky Error, odesílají je většinou moduly, které používáme a získávají se z callbacků jako první parametr, pokud u asynchronní funkce vznikne chyba. V našem případě zpracováváme jeden druh, chybu ValidationError, která vznikne při validaci na modulu mongoose. Pokud vzniklá chyba není ani potomkem AppError, ani potomkem Error, předává se k zpracování dále funkci next(). Vzhledem k tomu, že již další middleware pro zpracování chyby implementováno není, vrátí se prohlížeči chyba 500.

Může být také výhodné mít několik middleware pro ošetření chyb a každý nechat řešit pouze jeden typ chyby. Pak je ale důležité řadit middleware od nejspecifičtějšího po nejobecnější. To se hodí např. pokud má být middleware součástí znovupoužitelného balíčku.

Apiary

Bavíme-li se o REST v českém seriálu o Node.js, nelze nezmínit projet Apiary, který může výrazně pomoci při práci s REST API. Apiary pomáhá především ve třech oblastech: tvorbě dokumentace, vytvoření prototypu API a testování API.

Dokumentace

V administraci Apiary uživatel popisuje API pomocí jednoduchého jazyka. Každý blok požadavku a odpovědi začíná popisem, k čemu volání a odpověď slouží. Pro popis je možné využít syntaxi Markdown. Následně se uvádí HTTP metoda a URL, na které má požadavek jít a dále blok hlaviček a dat, které odpovídají HTTP požadavku (prefixují se znakem >) a blok hlaviček a dat, které odpovídají HTTP odpovědi (prefixují se logicky znakem <).

V administraci Apiary je k dispozici zvýraznění syntaxe, takže např. popis editace a mazání jedné stránky může vypadat takto:

Jednoduchá verze dokumentace pro náš projekt je dostupná na docs.zdrojak.apiary.io.

Server Mock

Po vytvoření dokumentace je k dispozici prototop API, který bude na HTTP požadavky odpovídat podle pravidel definovaných v dokumentaci. Náš testovací server je dostupný na adrese zdrojak.apiary.io, takže místo ostrého serveru je možné HTTP požadavky posílat sem a přijímat testovací HTTP odpovědi.

Prototypování API je nesmírně užitečné. V minulém díle bylo uvedeno několik bodů, kde se může API použít. Prototyp API je možné vytvořit mnohonásobně rychleji než programovat rovnou ostré API. Práci na projektu je možné začít definicí API a poté mohou oddělené týmy hned začít programovat části, které na API závisí, např. frontend webu, administraci, mobilní verzi pro různé platformy atd.

Prototyp API v Apiary má tak minimálně dvě výhody:

  • Není potřeba čekat na naprogramování celého API, ale je možné rovnou začít všechny práce najednou.
  • Návrh API lze snadno měnit s tím, jak přicházejí požadavky jeho implementátorů na změny a začít programovat až verzi API, která přesně odpovídá již vytvořené části aplikace, která s API komunikuje.

Testování

Když už máme vytvořené a dokumentované API, můžeme místo ostrého API serveru testovat prototyp API a sledovat rozdíly mezi očekávaným a skutečným chováním. K tomu slouží část Inspector.

Kromě zmíněných tří vlastností je k dispozici např. validace JSON Schema či Github integrace, kdy Apiary změny v dokumentaci sesynchronizuje s repozitářem.

Co dále

Příští díl bude věnován mnoha menším menším oblastem a nástrojům, které usnadňují vývoj Node.js aplikací. A podrobněji se také podíváme na dokumentování kódu a doporučované standardy při psaní kódu v Node.js.

Na tvorbě tohoto článku se svými připomínkami podílel také Pavel Lang (skolajs.cz) a Jakub Nešetřil (apiary.io). Díky!

Jakub pracoval na několika zajímavých projektech, za nejvýznamnější považuje vytvoření e-commerce řešení Shopio.cz. Poslední rok se plně věnuje Node.js, frameworku AngularJS a NoSQL databázím.

Čtení na léto

Jaké knihy z oboru plánujete přečíst během léta? Pochlubte se ostatním ve čtenářské skupině Zdrojak.cz na Goodreads.com.

Komentáře: 10

Přehled komentářů

Futrál Zpátky na stromy?
Jakub Mrozek Re: Zpátky na stromy?
Futrál REST?
Jakub Mrozek Re: REST?
Futrál REST vs. RPC
Jakub Mrozek Re: REST vs. RPC
Futrál Re: REST vs. RPC
Jakub Mrozek Re: REST vs. RPC
langpa CRUD ⊆ REST
Futrál Re: REST/CRUD vs. RPC
Zdroj: http://www.zdrojak.cz/?p=3742