Jeśli cokolwiek okaże się niejasne to zachęcam do zadawania pytań w komentarzach.
Budowa URL-a
Nie byłbym sobą, gdybym nie spróbował najpierw wyjaśnić kilku pojęć, którymi będę się dzisiaj posługiwał. Weźmy taki przykładowy adres internetowy:http://example.com/subpage?query=123&arg=val#home
Musimy umówić co do nazewnictwa poszczególnych fragmentów takiego adresu, aby łatwiej zrozumieć dalszą część artykułu :) Gotowi?
http | 
protokół | 
example.com | 
host/domena | 
/subpage | 
ścieżka (path) | 
?query=123&arg=val | 
query string | 
#home | 
fragment | 
Obsługa ścieżek w HapiJS
Kontynuujemy w miejscu, w którym skończyliśmy po poprzednim wpisie. Mamy działający serwer w HapiJS oraz jeden endpoint/:
server.route({  
    method: 'GET',
    path: '/',
    handler(request, reply) {
        reply('Hello, world!');
    }
});
Jak już wiemy, oznacza to, że pod ścieżką / zobaczymy napis Hello, world!.
Ścieżki statyczne
Możemy dodać kolejne endpointy zwracające inne odpowiedzi pod różnymi adresami. Przykładowo chcemy, aby zapytanieGET /hello zwracało napis Siema:
server.route({  
    method: 'GET',
    path: '/hello',
    handler(request, reply) {
        reply('Siema');
    }
});
W podobny sposób możemy zbudować dowolną statyczną ścieżkę, np.: /about/services/programming
Parametry
Co jednak w sytuacji gdy chcemy, aby nasz adres wyglądał na przykład tak:/users/michal, gdzie zamiast “michal” możemy wpisać dowolne imię? Z pomocą przychodzą parametry.
W HapiJS parametry definiujemy zamykając je wewnątrz klamerek {}:
path: /users/{name}
To już mamy. Aby dobrać się do wartości przekazanej jako parametr musimy zajrzeć do obiektu, którego jeszcze nie dotykaliśmy – request. Zawiera on taki obiekt jak request.params, a w nim wszystkie parametry danego URL-a.
Powyższy przykład zaimplementowalibyśmy w ten sposób:
server.route({  
    method: 'GET',
    path: '/users/{name}',
    handler(request, reply) {
        reply(request.params.name);
    }
});
Po otwarciu adresu http://localhost:3000/users/Michal naszym oczom powinno ukazać się wpisane imię, tutaj: “Michal”.
Wyświetlenie wartości request.params.name w ten sposób otwiera podatność na atak XSS. W kolejnych przykładach będę korzystał z funkcji encodeURIComponent, aby ten problem wyeliminować.
Parametry opcjonalne
server.route({  
    method: 'GET',
    path: '/users/{name?}',
    handler(request, reply) {
        if (request.params.name) {
            reply(encodeURIComponent(request.params.name));
        } else {
            reply('Anonymous');
        }
    }
});
Powyższy przykład zadziała zarówno dla ścieżki /users jak i /users/Michal.
Parametry fragmentaryczne
Możemy również zawrzeć w parametrze tylko część segmentu ścieżki, np.:path: '/photos/{name}.jpg'
Spowoduje, że wykonaniu GET /photos/michal.jpg pod zmienną request.params.name będzie tylko wartość “michal”.
Parametry wielosegmentowe
path: '/users/{name*2}'
Zadziała np. dla żądania GET /users/jan/kowalski. Zawartością zmiennej request.params.user będzie “jan/kowalski”.
Parametry catch-all
Czasami zachodzi również potrzeba przechwycenia po prostu całej ścieżki, którą wpisze użytkownik, niezależnie jak długa by ona nie była. W HapiJS możemy to zrobić z łatwością:path: '/users/{name*}'
Zadziała zarówno dla GET /users/jan, GET /users/jan/kowalski jak i GET /users/jan/kowalski/123/abc/def.
Obsługa query string w HapiJS
HapiJS obsługuje i automatycznie zamienia na obiekt również query string. Mamy do niego dostęp poprzezrequest.query. Spróbujmy:
server.route({  
  method: 'GET',
  path: '/search',
  handler(request, reply) {
    reply(request.query);
  }
});
Po wywołaniu GET /search?text=node.js&page=2&lang=pl wyświetli nam się w przeglądarce taki obiekt:
Walidacja żądań
Widzimy, że Hapi daje nam pewne możliwości parametryzacji ścieżek i zapytań. Czy jednak możemy jakoś konkretnie sprecyzować, że oczekujemy aby parametr{id} był liczbą, a parametr {name} literami? Albo, czy możliwe jest, żeby tylko wybrane pola z query string były akceptowane, a inne nie? Tak! Z pomocą przychodzi nam paczka Joi.
Joi
Joi jest biblioteką służącą do walidacji struktury obiektów zgodnie z podanymi parametrami. Między innymi, bo ponadto umożliwia również automatyczną zmianę nazw lub ignorowanie nieznanych pól, co jest bardzo przydatne przy zwracaniu odpowiedzi w formacie JSON. O tym kiedy indziej, a na razie przyjrzymy się Joi bliżej. Instalujemy:npm install joi --save
I dodajemy na samej górzej naszego pliku:
const Joi = require('joi');  
Walidacja w HapiJS
Teraz możemy przystąpić do konfigurowania walidacji w Hapi. Weźmy jeden z poprzednich przykładów, ścieżkę/users/{name?}. Z jakiegoś powodu chcemy, aby name zawierało w sobie wyłącznie liczby. Możemy to osiągnąć dodając nowe pole config.validate.params.name do definicji route’a:
Teraz zapytanieserver.route({ method: 'GET', path: '/users/{name?}', handler(request, reply) { if (request.params.name) { reply(encodeURIComponent(request.params.name)); } else { reply('Anonymous'); } },
// dodajemy: config: { validate: { params: { name: Joi.number() } } } });
GET /users/michal zakończy się błędem, natomiast GET /users/11 spowoduje wyświetlenie odpowiedzi. Jakiekolwiek zapytanie, które nie przejdzie walidacji automatycznie zostanie odrzucone z kodem błędu 400.
Złożona walidajca w Joi
W podobny sposób możemy dokonać również bardziej skomplikowanej walidacji. Wróćmy do endpointa/search. Chcemy, aby użytkownik zawsze musiał podać w query string pole text będące stringiem. Natomiast pola page i lang powinny być opcjonalne. page niech zawiera tylko liczby, a lang wyłącznie wybrane kody krajów: pl, gb lub de.
| pole | obowiązkowe? | dozwolone wartości | 
|---|---|---|
| text | ✓ | string | 
| page | ✗ | liczba | 
| lang | ✗ | tylko: pl, gb lub de | 
text: Joi.string().required()  
page: Joi.number()  
lang: Joi.only(['pl', 'gb', 'de'])  
Łącznie wygląda to tak:
server.route({  
  method: 'GET',
  path: '/search',
  handler(request, reply) {
    reply(request.query);
  },
  config: {
    validate: {
      query: {
        text: Joi.string().required(),
        page: Joi.number(),
        lang: Joi.only(['pl', 'gb', 'de'])
      }
    }
  }
});
I tak, przykładowo, zapytanie GET /search?text=abc&lang=us zwróci nam błąd, gdyż podany kod kraju nie jest jednym z dozwolonych. Warto też zwrócić uwagę na to, jak ten błąd wygląda i co dokładnie zawiera. W zwróconej odpowiedzi, oprócz kodu i nazwy błędu, jest też bardzo opisowe pole message, które w tym przypadku zawiera taki komunikat:
child "lang" fails because ["lang" must be one of [pl, gb, de]]
Jest to bardzo zrozumiała wiadomość, którą w zasadzie moglibyśmy wyświetlić użytkownikom (po jakimś sformatowaniu). Dodam jeszcze, że Joi posiada także możliwość definiowania własnych wiadomości o błędach.
Wartości domyślne
W powyższym przykładzie bardzo przydałaby się możliwość podania wartości domyślnych, skoropage i lang są opcjonalne. Joi również posiada taką opcję:
page: Joi.number().default(1)  
lang: Joi.only(['pl', 'gb', 'de']).default('pl')  
Więcej opcji…
Biblioteka Joi daje name ogromne możliwości wpływania na kształt odbieranych i wysyłanych obiektów, a jej integracja z frameworkiem HapiJS jest doskonała.Nie ma tutaj jednak miejsca na to, abym opisał wszystkie opcje konfiguracji Joi. Żądnych wiedzy odsyłam do dokumentacji Joi. Długiej, bo i opcji jest ogrom!
Przykładowy projekt
Posiedliśmy już wiedzę wystarczającą do wykonania prostej aplikacji w node.js z wykorzystaniem HapiJS. Chcielibyśmy, aby użytkownik miał możliwość dodawania nowych osób do swojej książki kontaktów. Każda osoba będzie składała się z imienia i nazwiska. Oczywiście, dodamy również opcję podejrzenia wszystkich kontaktów. Potrzebujemy do tego tylko kilku endpointów:GET /contacts | 
wyświetlenie kontaktów | 
POST /contacts | 
dodanie nowego kontaktu | 
{
    name: 'Jan',
    surname: 'Kowalski'
}
Implementacja
Najpierw tworzymy tablicę na kontakty. Aby nie odbiegać za mocno od tematu, na razie będą one przechowywane tak po prostu, w tablicy, a nie w bazie danych:const contacts = [];  
Ze względu na to, że kontakty przechowywane są w tablicy w pamięci, będą one dostępne w naszej aplikacji tylko do ponownego uruchomienia serwera.
Definiujemy zaprojektowane przed chwilą endpointy. Na pierwszy ogień idzieGET /contacts, gdyż ma po prostu zwracać kontakty:
server.route({  
  method: 'GET',
  path: '/contacts',
  handler(request, reply) {
    reply({
      contacts
    });
  }
});
POST /contacts musi pobrać przesłane dane i dodać je do tablicy. Do tego wymagana jest krótka walidacja. Na koniec zwracamy odpowiedź z kodem HTTP 201, który oznacza pomyślne utworzenie kontaktu:
server.route({  
  method: 'POST',
  path: '/contacts',
  config: {
    validate: {
      payload: Joi.object({
        contact: Joi.object({
          name: Joi.string().required(),
          surname: Joi.string().required()
        }).required()
      })
    }
  },
  handler(request, reply) {
    const contact = request.payload.contact;
    contacts.push(contact);
    reply({contact}).code(201);
  }
});
Korzystamy tutaj z request.payload, o którym wcześniej nie wspominałem. Jest to po prostu body wysłanego żądania POST.
Testowanie backendu
I już! Utworzyliśmy pierwszy w pełni funkcjonalny backend do aplikacji. Jak go przetestować? O bardziej zaawansowanych metodach opowiem w jednym z kolejnych wpisów, a na razie zróbmy to po prostu z konsoli przeglądarki.Otwieramy adres http://localhost:3000/contacts – powinniśmy zobaczyć odpowiedź z serwera. Na razie jest to obiekt z pustą tablicą:
W konsoli przeglądarki wykonujemy proste żądanie POST aby dodać nowy kontakt:
fetch('/contacts', {  
    method: 'POST',
    body: JSON.stringify({  
        contact: { name: 'Jan', surname: 'Kowalski' }  
    })
});
Teraz po odświeżeniu strony zobaczymy dodane kontakty!
Przy okazji podpowiedź: Warto zainstalować do swojej przeglądarki rozszerzenie formatujące JSON. Wszystko staje się wtedy znacznie bardziej czytelne:
Podsumowanie
Uff, dzisiaj spora dawka wiedzy za nami. Nauczyliśmy się obsługiwać parametry w ścieżkach oraz query string. Poznaliśmy bibliotekę Joi i nauczyliśmy się ją wykorzystywać do walidowania zapytań do naszego API. Zwieńczeniem lekcji było stworzenie backendu do prostej aplikacji w node.js.Cały kod dostępny jest na moim GitHubie: https://github.com/mmiszy/hapijs-tutorial/tree/czesc-2
W kolejnym wpisie wykorzystamy Joi do zmieniania kształtu danych, które są zwracane przez nasze API. Ponadto, zmienimy trochę strukturę naszej aplikacji i podzielimy plik index.js na logiczne fragmenty.