Openapi generator – REST client dla Angular
Tworząc aplikacje www bardzo często piszemy tzw. backend i frontend. Może wówczas (ale nie musi!) powstać problem związany z API do komunikacji pomiędzy tymi dwiema warstwami.
Bardzo często odbywa się to na zasadzie dogadujemy się jak wygląda endpoint, ustalamy jego ścieżkę (adres), parametry wejściowe, wyjściowe i metodę komunikacji. Możemy to spisać ale ostatecznie i tak narażamy się na „nieprzewidziane” zmiany. Dodatkowo wybierając tą metodę zmuszeni jesteśmy do pisania elementów API po obu stronach (zwiększając możliwość popełnienia błędu).
Są jednak narzędzia które umożliwiają automatyczne generowanie kodu dla jednej ze stron komunikacji. Dzięki temu raz że oszczędzamy czas, a dwa zmniejszamy ryzyko popełnienia błędu.
Zaraz pokażę, jakich narzędzi użyć (swagger, openapi) aby dla serwera www (node + express) wygenerować gotowy moduł który można użyć w aplikacji napisanej za pomocą frameworka Angular.
Do dzieła!
Na początek piszemy nasz serwer www
Serwer na potrzeby artykułu jest bardzo prosty. Posiada proste metody CRUD, możliwość przetwarzania formatu json oraz zezwala na cross origin.
const express = require('express'); const cors = require('cors'); const app = express(); const port = 3000; let data = [{id: 1, name: 'Mateusz'}]; app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors()); app.listen(port, () => { console.log(`API app listening at http://localhost:${port}`); }); app.get('/', (req, res) => { res.send(data); }); app.get('/:id', (req, res) => { res.send(findItemById(req.params.id)); }); app.post('/', (req, res) => { if (!req || !req.body || !req.body.id || findItemById(req.body.id)) { res.status(400).send('error while add item'); return; } data.push(req.body); res.status(201).send(req.body); }); app.put('/:id', (req, res) => { const editedItem = findItemById(req.params.id); if (!editedItem) { res.status(400).send('error while edit item'); return; } if (!req || !req.body || !req.body.id) { res.status(400).send('error while edit item'); return; } data = data.map((item) => item.id !== editedItem.id ? item : req.body); res.status(204).send(); }); app.delete('/:id', (req, res) => { const deletedItem = findItemById(req.params.id); if (!deletedItem) { res.status(400).send('error while delete item'); return; } data = data.filter((item) => item.id !== deletedItem.id); res.status(204).send(); }); function findItemById(id) { return data.find((item) => item.id === parseInt(id)); }
Teraz dla naszego kodu chcielibyśmy wygenerować swaggerowy plik json. Przyda nam się do automatycznego wygenerowania biblioteki z api.
Dokumentowanie kodu
Do generowania plików swaggera użyjemy narzędzia swagger-jsdoc
npm i swagger-jsdoc -D
Na początku musimy napisać dla naszych endpointów dokumentację jsdoc.
/** * @swagger * components: * schemas: * DataItem: * type: object * properties: * id: * type: integer * format: int32 * name: * type: string */ /** * @swagger * /: * get: * responses: * 200: * description: List of DataItem * content: * application/json: * schema: * type: array * items: * $ref: '#/components/schemas/DataItem' */ app.get('/', (req, res) => { res.send(data); });
Generowanie swagger json
A następnie napisać kawałek kodu który będzie dla nas generował plik json z naszymi API.
const swaggerJSDoc = require('swagger-jsdoc'); const fs = require('fs'); const swaggerDefinition = { openapi: "3.0.0", info: { title: "api-gen-node-angular", version: "1.0.0" } }; const options = { swaggerDefinition, apis: ['./app.js'], }; const swaggerSpec = swaggerJSDoc(options); fs.writeFile('swagger.json', JSON.stringify(swaggerSpec, null, 2), (err) => { if (err) { console.error('error while save swagger.json', err); return; } console.log('swagger.json saved!'); });
Powyższy kod przy pomocy narzędzia swagger-jsdoc
wygeneruje nam plik swagger.json
. W polu apis
obiektu options
podajemy listę plików z których chcemy z czytać dokumentację. Biblioteka utworzy nam zmienną z obiektem API swaggerSpec
którą zapisujemy do pliku swagger.json
. Skrypt zapisałem jako swagger-jsdoc.js
. Następnie możemy go wywołać i utworzyć plik json.
node swagger-jsdoc.js
Teraz w naszym folderze domowym powinien utworzyć się plik json z definicjami endpointów naszego serwera. Pierwszy krok za nami, teraz chcielibyśmy utworzyć bibliotekę dla naszego API, którą wykorzystamy w aplikacji angularowej.
Generowanie klienta api z automatu
Do wygenerowania biblioteki użyjemy narzędzia openapi-generator-cli
npm i @openapitools/openapi-generator-cli -D
Następnie tworzymy plik z konfiguracją generatora. Plik json o nazwie openapitools.json
w głównym folderze naszego projektu
{ "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { "version": "4.3.1", "generators": { "angular": { "generatorName": "typescript-angular", "output": "../openapi", "glob": "swagger.json", "additionalProperties": { "ngVersion": "8.0.0", "npmName": "RestApiClient", "supportsES6": true, "npmVersion": "1.0.0", "withInterfaces": true } } } } }
Najważniejsza dla nas rzecz to obiekt generators
. W nim umieszczamy definicję dla naszych bibliotek które chcemy wygenerować. Pierwsza rzecz to nazwa generatora (w powyższym przykładzie angular
) i jest tutaj pełna dowolność nazwy. Następnie w takim obiekcie definiujemy co chcemy wygenerować generatorName
, jak widać wybrałem typescript-angular
aby wygenerować bibliotekę z modułem Anularowym. Kolejny element to output
czyli miejsce gdzie chcę aby wygenerowane pliki się znalazły. Ostatnim istotnym elementem jest glob
i jest to nazwa pliku z definicjami naszego API. Ostatni element to additionalProperties
i są to dodatkowe parametry już dla konkretnego generatora. W moim przykładzie wygenerowana zostanie biblioteka z Angularem w wersji 8.0.0
, biblioteka nazywać się będzie RestApiClient
i będzie miała wersję 1.0.0
.
Po napisaniu konfiguracji generujemy bibliotekę.
openapi-generator-cli generate
Użycie klienta API w projekcie Angularowym
Ostatnią rzeczą jaka nam pozostała, to użyć wygenerowaną bibliotekę w naszym projekcie Angularowym. Aby to zrobić musimy jeszcze wygenerowane pliki klienta skompilować i udostępnić dla naszego projektu. Przechodzimy do folderu z wygenerowanym kodem i uruchamiamy te dwa polecenia.
npm i npm run build
Po wykonaniu tych poleceń do folderu dist
zostaną skopiowane skompilowane pliki naszej biblioteki. Teraz możemy już taką bibliotekę np umieścić w naszym repozytorium (npm publish
) i korzystać z niej w dowolnych aplikacjach.
Ja wykorzystam natomiast polecenie npm link
które służy do linkowania lokalnie (na mojej maszynie) bibliotek npm, z których będę później mógł korzystać.
npm link dist
Wywołanie powyższego polecenia spowoduje podlinkowanie zawartości folderu dist
jako elementu npma. Teraz w naszym projekcie Angularowym możemy ponownie za pomocą npm link
podlinkować tą bibliotekę i z niej korzystać.
npm link RestApiClient
Różnica tylko taka, że wcześniej podawałem co chce udostępnić przez linkowanie, a teraz podałem już jakiego elementu chce używać.
Pozostał ostatni element naszej układanki, czyli implementacja biblioteki. Pierwsze co należy zrobić to zaimportować moduł naszej biblioteki.
import {ApiModule, Configuration} from 'RestApiClient'; ... @NgModule({ ..., imports: [ ... ApiModule.forRoot(() => new Configuration({basePath: 'http://localhost:3000'})), HttpClientModule, ], }) export class AppModule { }
Jak widzimy należy dodać wpis w tablicy imports
modułu naszej aplikacji. Dla najprostszego użycia wystarczyłoby samo ApiModule
ale my używamy serwera na innym porcie niż uruchamia nam się lokalnie aplikacja Angular, dlatego podaje basePath
do serwera.
Mając moduł z naszym API zaimportowany do aplikacji możemy z niego teraz korzystać. Wystarczy wstrzyknąć serwis w miejscu w którym chcemy z niego korzystać i wywołać metody które udostępnia.
import {Component, OnInit} from '@angular/core'; import {DataItem, DefaultService} from 'RestApiClient'; import {Observable} from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent implements OnInit { public data$: Observable<DataItem[]>; constructor(private defaultService: DefaultService) { } public ngOnInit(): void { this.data$ = this.defaultService.rootGet(); } }
<div> <div *ngFor="let data of data$ | async" class="data-item"> {{data.name}} <span>({{data.id}})</span> </div> </div>
Teraz już tylko uruchamiamy nasze aplikacje i możemy w przeglądarce zobaczyć efekt naszej pracy!
Kod wykorzystany w artykule dostępny jest na moim githubie: https://github.com/lewyino/api-gen-node-angular