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