Jeśli cokolwiek okaże się dla Ciebie niejasne to zadaj mi pytanie w komentarzach.
Joi
W poprzednim wpisie pokazałem jak korzystać z podstawowych możliwości biblioteki Joi. Dla przypomnienia: Jest to biblioteka pozwalającą na walidację żądań zgodnie z podaną strukturą.Nie mówiłem jednak o tym, że takie obiekty Joi mogą posłużyć do automatycznego generowania dokumentacji API. Brzmi ciekawie? Jest to niezwykle przydatna możliwość i korzystałem z niej wiele razy w różnych projektach.
Przypomnienie
Wróćmy do kodu z poprzedniej części kursu. Mieliśmy tam zdefiniowane kilka endpointów:GET /GET /users/{name?}GET /photos/{name}.jpgGET /searchGET /contactsPOST /contacts
POST /contacts wymagane było podanie obiektu z polem contact będącym obiektem z obowiązkowymi polami name i surname. Wyglądało to tak:
server.route({  
  method: 'POST',
  path: '/contacts',
  config: {
    validate: {
      payload: Joi.object({
        contact: Joi.object({
          name: Joi.string().required(),
          surname: Joi.string().required()
        }).required()
      })
    }
  },
  …
});
Pamięć odświeżona? To świetnie, gdyż na podstawie takich definicji za moment automatycznie wygeneruje Ci się dokumentacja.
Swagger UI
Swagger UI jest narzędziem służącym do wizualizacji API. Działa z wieloma różnymi platformami, a sposób opisywania API w formacie Swagger jest niezwykle powszechny (przy okazji warto wspomnieć, że standard Open API bazuje na Swaggerze). Nie jest zaskoczeniem, że hapi również posiada wtyczkę dodającą Swagger UI:hapi-swagger. Zainstalujmy ją:
npm install hapi-swagger --save
Dodatkowymi potrzebnymi nam zależnościami są inert i vision służące do obsługi template'ów i serwowania statycznych plików:
npm install inert vision --save
Następnie zainstalowane wtyczki musimy dodać do Hapi. Pisanie własnych pluginów jest tematem bardzo rozbudowanym, aby jednak skorzystać z gotowych wtyczek wystarczy, że użyjemy funkcji server.register. Do niej przekazujemy tablicę pluginów:
const Inert = require('inert');  
const Vision = require('vision');  
const HapiSwagger = require('hapi-swagger');
server.register([  
  Inert,
  Vision,
  {register: HapiSwagger, options}
], err => {
  if (err) {
    throw err;
  }
  server.start((err) => {
    if (err) {
      throw err;
    }
    console.log(`Server running at ${server.info.uri}`);
  });
});
Zauważ, że HapiSwagger rejestrujemy nieco inaczej niż Inert i Vision. Dzieje się tak dlatego, że do Swaggera potrzebujemy przekazać dodatkową konfigurację. Definiujemy więc wcześniej stałą options:
const pkg = require('./package.json');  
const options = {  
  info: {
    title: pkg.description,
    version: pkg.version
  }
};
Zwróć też uwagę, że wywołanie server.start przeniosłem do środka callbacka funkcji server.register. Jest to wymagane, aby serwer wiedział o wszystkich wtyczkach w momencie uruchomienia.
To wystarczy, aby nasza dokumentacja była automatycznie generowana. Uruchom serwer i otwórz adres http://localhost:3000/documentation Twoim oczom powinien się ukazać widok podobny do tego:
Jednak nie ma tutaj jeszcze żadnego endpointa! Dlaczego? Aplikacje napisane w HapiJS mogą być mieszanką choćby statycznych stron i REST API. Dokumentować chcemy tylko REST API, dlatego musimy dodatkowo powiedzieć Hapi, które końcówki są naszym API, a które nie. Musimy otagować odpowiednie route'y poprzez dodanie do nich właściwości tags: ['api']:
server.route({  
  method: 'POST',
  path: '/contacts',
  config: {
    tags: ['api'],
    …
  },
  …
});
Gdy to zrobisz, wreszcie dokumentacja będzie generowana prawidłowo dla wybranych endpointów:
Dodatkowe informacje
Weźmy pod lupę przykładową dokumentację dwóch endpointów:
Z prawej strony w polu Data Type widzimy informacje, które podaliśmy do walidatora Joi. W pola body oraz w Parameters poniżej można wpisać dowolne wartości i przetestować działanie API. To świetna możliwość!
Zauważcie, że pole, w którym jasno zdefiniowaliśmy tylko 3 poprawne wartości (pl, gb, de) nie jest inputem, lecz selectem: hapi-swagger dobrze sobie z tym poradził :)
Jednak brakuje mi tu kilku rzeczy. Bez wątpienia przydałby się opis każdego endpointa, prawda? Dodatkowo, pomocna byłaby informacjach o danych zwracanych czy też możliwych kodach błędów. Czy takie informacje również możemy podać w Hapi? Owszem!
Opis i notatki
Opis oraz dodatkowe notatki możesz podać w jako poladescription i notes w konfiguracji route'a. Notatki mogą zawierać dowolne przydatne informacje, np. krótkie podsumowanie zwracanych danych albo podpowiedź odnośnie czegoś, co może zaskoczyć przy pracy z tym endpointem.
server.route({  
  method: 'POST',
  path: '/contacts',
  config: {
    tags: ['api'],
    description: 'Create a new contact',
    notes: 'Returns created contact',
    …
  },
  …
});
Zwracane dane
Aby opisać dane zwracane przez endpoint, również możemy skorzystać z Joi. Dodaj poleresponse.schema w konfiguracji route'a:
server.route({  
  method: 'POST',
  path: '/contacts',
  config: {
    …
    response: {
      schema: Joi.object({
        contact: {
          name: Joi.string().required().example('Jan'),
          surname: Joi.string().required().example('Kowalski')
        }
      }).label('CreateContactResponse')
    }
  },
  …
});
Korzystamy tutaj z dwóch nowych funkcji, których nazwy raczej mówią już wszystko: example oraz label. example sprawia, że w dokumentacji pojawi się konkretny przykład wartości. Jest to przydatne, gdy chcemy opisać coś precyzyjnie i jasno. label zaś umożliwia nadanie unikatowej nazwy tej strukturze danych w celach łatwiejszej identyfikacji.
Duplikacja kodu
Na pewno zauważyłeś, że dane przyjmowane przez ten endpoint i dane przez niego zwracane mają dokłądnie tę samą strukturę – przyjmujemy kontakt i zwracamy kontakt. Można by się pokusić o przeniesienie schemy'y Joi do osobnej stałej i użycie jej dwukrotnie:const ContactSchema = Joi.object({  
  contact: Joi.object({
    name: Joi.string().required().example('Jan').description(`Contact's name`),
    surname: Joi.string().required().example('Kowalski').description(`Contact's surname`)
  }).required().label('Contact')
}).required().label('ContactSchema');
Dodatkowo do imienia i nazwiska dodałem description, czyli krótki tekst opisujący każde z pól osobno. W tym przypadku wydaje się on całkowicie zbędny, jednak warto pamiętać, że taka możliwość istnieje.
Kody błędów
W prawdziwej aplikacjie niektóre endpointy będą zwracały wiele różnych kodów błędów. Przykładowo gdy na portalu społecznościowym spróbuję zmodyfikować profil X, mogę dostać takie błędy:- 400 – błędne dane w żądaniu (np. imię to liczba)
 - 401 – niezalogowany (a niezalogowani nie mogą niczego edytować)
 - 404 – nie ma takiego zasobu (gdy X nie istnieje)
 - 403 – brak dostępu do zasobu (gdy X to nie mój profil)
 
hapi-swagger również pozwala na opisanie tych statusów, jednak składnia, która do tego służy jest moim zdaniem nieco… dziwna. Ale mimo to spróbuj dodać dwa kody błędów do dokumentacji endpointa: 400 i 409 (w przypadku gdy użytkownik o podanym imieniu i nazwisku już istnieje):
server.route({  
  method: 'POST',
  path: '/contacts',
  config: {
    plugins: {
      'hapi-swagger': {
        responses: {
          400: {
            description: 'Bad request'
          },
          409: {
            description: 'User with given name/surname exists'
          }
        }
      }
    },
    …
  },
  …
});
Testowanie bezpośrednio w Swaggerze
W wygenerowanej dokumentacji na pewno Twoją uwagę przykuł formularz oraz przycisk "Try it out". Wspominałem już, że Swagger UI pozwala na testowanie API bezpośrednio na stronie z dokumentacją. Dopiszemy teraz brakującą funkcję (sprawdzanie czy kontakt już istnieje) i zwrócimy odpowiedni kod błędu, a następnie przetestujemy to zachowanie bezpośrednio w Swaggerze.Kod
Dopisanie tego fragmentu kodu jest banalnie proste. W tablicy kontaktów szukamy po imieniu i nazwisku i jeśli coś znajdziemy to zwracamy błąd, a jeśli nie, to dodajemy kontakt tak jak dotychczas. Do zwrócenia błędu posłuży namreply().code(409):
Teraz nowo dodaną funkcję możemy sprawdzić w Swagger UI:handler(request, reply) { const contact = request.payload.contact; const userExists = contacts.find(c => c.name === contact.name && c.surname === contact.surname); if (userExists) { return reply('This user exists!').code(409); }
contacts.push(contact); reply({contact}).code(201); }
Podsumowanie
Dzisiaj nauczyliśmy się dodawać dokumentację do REST API. Brzmi banalnie, ale możliwości, które dają nam Hapi, Joi i Swagger UI są ogromne.Cały kod jest dostępny na moim GitHubie: https://github.com/mmiszy/hapijs-tutorial/tree/czesc-3
Joi i hapi-swagger posiadają niezliczone różne opcje i nie sposób tutaj wszystkich wymienić. Dlatego zachęcam do poczytania oficjalnych dokumentacji:
Jeśli masz wrażenie, że po dodaniu dokumentacji do kodu cały plik index.js mocno napęczniał – to słusznie ;) Ten wpis miały być poświęcony strukturze, ale generowanie dokumentacji wydało mi się ciekawsze. A więc następnym razem zrobimy z tym w końcu porządek!Masz pytania? Zadaj je w komentarzu pod tekstem.