Tworzenie REST API w node.js z wykorzystaniem frameworka HapiJS – część 3 – dokumentacja API

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

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 namesurname. 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ą inertvision 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ż InertVision. 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 descriptionnotes 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.

  • > format opisywania API w formacie Swagger stał się już niemal standardem

    Tak w sumie to jest standardem – i to pod opieką Linux Foundation 😉 https://www.openapis.org/

    • Bardziej mnie zastanawia czemu nie wytknąłeś tego oczywistego błędu „format (…) w formacie” 😉
      Obie rzeczy zaraz poprawię. Przez stwierdzenie „jest niemal standardem” nie miałem na myśli, że kandyduje do bycia formalnym standardem, tylko że po prostu jest bardzo popularny!

      • Pewnie dlatego, że jest zbyt oczywisty 😉

        Rozumiem, co miałeś na myśli. Niestety, w naszej branży jest pełno takich „standardów”.