Openapi generator – REST client dla Angular

Opublikowane przez lewy w dniu

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