Přejít k navigační liště

Zdroják » Různé » JavaScript na serveru: implementace REST API

JavaScript na serveru: implementace REST API

Články Různé

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

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!

Komentáře

Subscribe
Upozornit na
guest
10 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Futrál

Rozlišovat HTTP metody procedurálně pomocí IFů a porovnávání textových řetězců? Kde to jsme? Totéž podporované MIME typy a vyhazování výjimek. Vždyť tohle jde na jiných platformách krásně deklarativně pomocí pár anotací — a člověk se pak může soustředit na vlastní obchodní logiku. Tohle mi přijde jako krok zpět…

Futrál

Ještě k předchozímu dílu:

„Např. URL pro vygenerování faktury pro objednávku číslo 123 může mít adresu: /example.com/or­ders/123/gene­rate Akce se bude volat metodou HTTP POST, protože žádná akce GET nesmí data měnit.“

Tím se z toho vlastně stává RPC – už neděláme CRUD operace nad zdroji, ale voláme vzdálené procedury: generujFakturu();

„Chceme-li se odkázat na dokument, který závisí na jiném dokumentu a nemůže existovat zvlášť, přidáme do cesty i rodičovský dokument. Např. varianty produktů nemohou existovat samostatně, takže na variantu s ID 456 produktu ID 123 se můžeme odkázat takto: example.com/pro­ducts/123/vari­ants/456“

Takže objednávka by měla pak mít URL třeba /customer/123/or­der/456 nebo výrobek: /supplier/123/pro­duct/456 – protože objednávka neexistuje bez zákazníka a výrobek bez výrobce. Nakonec bychom do toho URL mohli zahrnout i další entity, bez kterých by to nemohlo existovat… Což by asi nebylo dvakrát rozumné.

Futrál

A tam vysvětlíte (mimochodem, musíme si vykat?), jak zavolat proceduru (generování faktury) pomocí CRUD/RESTu?

(ano, vím, že to jde, i jak to jde, ale je to zneužití té technologie resp. použití technologie, která nepasuje na zadání)

Futrál

Chápu, že v praxi se prasí mnohem víc (např. se používají GET metody i pro změny stavu, protože klient prostě nic jiného poslat neumí), chápu i to, že se to směrem k zákazníkovi prezentuje jako REST, protože to teď letí a každý to musí mít. Ale nechápu, proč je potřeba si takhle lhát i v článcích a mezi programátory – copak je tak těžké si přiznat, že REST se nehodí na všechno a ne vše se pomocí něj dá realizovat? Vždyť i tak je to dobrá technologie, jen není univerzální (ale to není žádná). Takže navrhuji tomu (těm procedurálním částem) přestat říkat REST nebo použít nějakou jinou vhodnější technologii pro volání procedur – místo hraní divadýlka a předstírání, že tu jsou nějaké „procedurální zdroje“, a vymýšlení pseudoteorií, které obhájí, že to je ještě REST, abychom nemuseli přiznat, že REST nejde použít na vše.

A to přirovnání k HTML formulářům je naprosto nesmyslné – formulář typicky je CRUD práce se zdrojem, např. to založení objednávky. Případně to není práce se zdrojem (např. složitější vyhledávání nebo zadání parametrů pro výpočet něčeho na serveru – třeba zadáme rozměry a materiály a server nám vypočte cenu), ale tomu zase nikdo (rozumný) neříká REST, protože to REST není a je to normální RPC pomocí HTTP POST a HTML formuláře.

Pavel Lang

CRUD ⊆ REST.

REST může obsahovat „magii“ v podobě volání procedur, někdy to je vhodné. Na implementaci samotného CRUD stačí nějaká NoSQL databáze, možná CouchDB a nemusí se nic řešit. Protože je ale potřeba nad daty vykonávat netriviální akce, je třeba to „nějak“ udělat a ten název kontroleru na konci URL není zase tak špatný nápad. Ano, má to blízko k RPC, ale neznamená to, že to už není REST. Naopak, to si myslím, že je rozdíl mezi syrovým CRUD a REST, tenhle kousek navíc…

A hlavně REST není standart, není to ani doporučení, REST je styl architektury software a to velice volný, založený pouze na principech HTTP.

Pro ty, kteří chtějí pevnější a typový návrh jsou tu web services popsané pomocí WSDL, WADL nebo RPC protokoly…

Futrál

P.S. ještě citace z článku, který odkazujete v prvním díle:

Pomocí REST lze ovládat i stav aplikace, pokud jej dokážeme popsat takovým způsobem, že si vystačí s modelem „zdroje – CRUD akce“.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.

Pocta C64

Za prvopočátek své programátorské kariéry vděčím počítači Commodore 64. Tehdy jsem genialitu návrhu nemohl docenit. Dnes dokážu lehce nahlédnout pod pokličku. Chtěl bych se o to s vámi podělit a vzdát mu hold.