Skocz do treści

Już wkrótce odpalamy zapisy na drugą edycję next13masters.pl. Zapisz się na listę oczekujących!

REST API w node.js z HapiJS – dokumentacja API

Dalej na temat tworzenia backendu w node.js z wykorzystaniem HapiJS. Ten wpis jest o automatycznym generowaniu dokumentacji do endpointów. Zapraszam!

Ten artykuł jest częścią 4 z 5 w serii HapiJS.

Zdjęcie Michał Miszczyszyn
JavaScript3 komentarze

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}.jpg
  • GET /search
  • GET /contacts
  • POST /contacts
Do nich dodawaliśmy walidację w Joi. Przykładowo chcieliśmy, aby do endpointa 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 pola description 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',},});
screenshot

Zwracane dane

Aby opisać dane zwracane przez endpoint, również możemy skorzystać z Joi. Dodaj pole response.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)
…i tak dalej, i tym podobne. 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 nam reply().code(409):
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); }

Teraz nowo dodaną funkcję możemy sprawdzić w Swagger UI:

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.

👉  Znalazłeś/aś błąd?  👈Edytuj ten wpis na GitHubie!

Autor