Podejście API-First wraz generatorem Open API

Filip Raszka

Kiedy w przeszłości pisano aplikacje typu serwer-klient, zwykle trzeba było wykonać pewną pracę wokół kontraktu API. Back-end i front-end uzgadniały (lub powinny były uzgodnić) wspólny kontrakt, a następnie obie strony implementowały swoje odpowiednie modele, usługi, kontrolery itp. Przy optymistycznym założeniu tylko kilka problemów zostałoby później znalezionych podczas integracji, takich jak choćby inaczej nazwane pola. W najgorszym wypadku całe sekcje kodu byłyby zasadniczo niekompatybilne, co wymagałoby dużo pracy później. Problemy w takim przypadku mogłyby się nawet pogłębić, gdyby konieczne były późniejsze zmiany. Takie problemy były bardzo powszechne i chociaż podejście API-First, wraz z generatorami API nie usuwa ich całkowicie, to znacznie je redukuje, wprowadzając strukturę i porządek do kodu związanego z komunikacją.

Podejście API-First

W tym podejściu do rozwoju priorytetowo traktujemy interfejsy API. Rozumiemy, że są one niezbędne dla projektu i jako takie stanowią produkt sam w sobie. Musimy je starannie zaplanować, biorąc pod uwagę każdą część systemu – nie są one bowiem produktem ubocznym back-endu, a raczej częścią kontraktu i fasadą dla całego projektu aplikacji. Dzięki podejściu API-First możemy użyć języka opisującego API, takiego jak YAML ze specyfikacją OpenAPI, aby stworzyć wspólną specyfikację, która następnie może być używana zarówno przez back-end, front-end, jak i urządzenia mobilne, zapewniając korzystanie z tych samych kontraktów.

Zalety podejścia API-First ze wspólną specyfikacją OpenAPI:

  • Zapewnia korzystanie z tych samych umów przez wszystkie części systemu.
  • Ułatwia współpracę między back-endem i front-endem, ułatwiając równoległy rozwój.
  • Automatyzuje generowanie powtarzalnych elementów, takich jak modele, usługi i kontrolery, zmniejszając ilość standardowego kodu, organizując strukturę i oszczędzając czas programistów.
  • Automatycznie generuje i udostępnia dokumentację API.

Specyfikacja OpenApi YAML

Jest to przykład jednego z najpopularniejszych języków opisu API, open-api 3.0 w YAML:

openapi: "3.0.0"
info:
  version: 2.0.0
  title: Example Task API
  description: |
    This specification contains example Task endpoints
servers:
  - url: http://localhost:8080
paths:
  /task:
    description: |
      Create new task
    post:
      tags:
        - "Task"
      operationId: createTask
      requestBody:
        description: Create a new task
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTaskRequest'
            example:
              {
                "name": "Example Task",
                "description": "Example Task Description",
                "priority": 1
              }
      responses:
        "200":
          description: Ok. The successful response contains ID of the newly created task
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateTaskResponse"
              example:
                {
                  "id": "bd468b42-f06a-4b22-aa8a-2f0c16fe60b4"
                }
    get:
      tags:
        - "Task"
      operationId: getTasks
      parameters:
        - name: name
          in: query
          description: task name filter
          allowEmptyValue: true
          schema:
            type: string
          example: Task
        - name: priority
          in: query
          description: task priority filter
          allowEmptyValue: true
          schema:
            type: integer
          example: 1
      responses:
        "200":
          description: Ok. The successful response contains the list of 'FileGroupDTO's
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Task"
              example:
                [
                  {
                    "id": "bd468b42-f06a-4b22-aa8a-2f0c16fe60b4",
                    "name": "Task 1",
                    "priority": 1,
                    "description": "Task 1 description"
                  },
                  {
                    "id": "cd468b42-f06a-4b22-aa8a-2f0c16fe60b4",
                    "name": "Task 2",
                    "priority": 2,
                    "description": "Task 2 description"
                  }
                ]
components:
  schemas:
    CreateTaskRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 2
          maxLength: 64
        priority:
          type: integer
        description:
          type: string
      required:
        - name
        - priority
    CreateTaskResponse:
      type: object
      properties:
        id:
          type: string
      required:
        - id
    Task:
      $ref: "./components/Task.yml"

Możemy zauważyć trzy główne części pliku: część metadanych, ścieżki i sekcję komponentów. Skupię się na tych trzech sekcjach, chociaż istnieje wiele dodatkowych typów obiektów OpenApi, obsługiwanych przez specyfikację. Pełną listę można znaleźć tutaj.

Metadane

W tej sekcji znajdują się informacje o: – wersji używanej specyfikacji (openapi: „3.0.0”). – wersji, nazwie i opisie tego konkretnego API (info). – adresach serwerów (servers). Niektóre narzędzia do generowania klientów ustawią te adresy jako domyślne. Generatory Java zazwyczaj ignorują tę część.

Ścieżki

Jest to najważniejsza część, w której definiujemy nasze endpointy. Dla każdej ze ścieżek możemy zdefiniować wiele metod. Definicja metody może składać się z: – tagów: Logicznego kwalifikatora grupowania operacji. Wiele narzędzi generujących będzie używać tego pola do nazywania usług i przypisywania do nich metod. – operationId: Unikalny ciąg znaków używany do identyfikacji operacji. Wiele narzędzi do generowania użyje tego pola do nazwania rzeczywistej metody serwisu. – opcjonalne parametry: Lista parametrów (zapytanie, ścieżka, nagłówki) mających zastosowanie dla danej operacji. – opcjonalna treść żądania: Treść żadania dla danej operacji – odpowiedzi: Lista odpowiedzi, identyfikowanych przez status odpowiedzi http (mogą istnieć różne definicje dla różnych kodów). Dla określonego kodu statusu możemy dodać opis i definicję treści (jak i treść odpowiedzi), wraz z przykładem w JSON.

Komponenty

Jest to mała „biblioteka” służąca do definiowania API. Możemy tu definiować obiekty wielokrotnego użytku, takie jak modele, do których możemy się później odwoływać. Bez wyraźnych odniesień ta część nie ma wpływu na API.

Odwołania do obiektów w innych plikach

W celach organizacyjnych i refaktoryzacji możemy zdefiniować obiekty OpenAPI w innych, osobnych plikach i połączyć je z głównym plikiem w następujący sposób:

$ref: "./components/Task.yml"

Jest to szczególnie przydatne, gdy nasza specyfikacja API staje się duża. Plik zewnętrzny może mieć następującą zawartość:

type: object
properties:
  id:
    type: string
  name:
    type: string
  priority:
    type: integer
  description:
    type: string
required:
  - id
  - name
  - priority

Używanie generatora Open API Generator w Springu

Chcąc dodać generator OpenAPI do naszego projektu Spring, czyli jeden z najlepszych generatorów OpenAPI, musimy dodać tylko jedną wtyczkę do naszej konfiguracji maven’a w pom.xml:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>6.6.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>
                    ${project.basedir}/src/main/resources/spec/task-api.yml
                </inputSpec>
                <generatorName>spring</generatorName>
                <apiPackage>com.jblog.openapiexample.api</apiPackage>
                <modelPackage>com.jblog.openapiexample.model</modelPackage>
                <supportingFilesToGenerate>
                    ApiUtil.java
                </supportingFilesToGenerate>
                <configOptions>
                    <useSpringBoot3>true</useSpringBoot3>
                    <delegatePattern>true</delegatePattern>
                    <openApiNullable>false</openApiNullable>
                    <interfaceOnly>false</interfaceOnly>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

Domyślnie narzędzie generuje dla nas modele, interfejsy API i kontrolery. Wygenerowany kod będzie już posiadał walidację i adnotacje swagger’a.

Konfiguracja

W sekcji konfiguracji pluginu musimy określić lokalizację naszej specyfikacji. Zazwyczaj będzie to plik z katalogu resources. Następnie możemy ustawić pakiety dla api i modelu, gdzie zostanie wygenerowany kod. Opcjonalnie podać dodatkowe opcje konfiguracyjne. Narzędzie jest bardzo wszechstronne i istnieje wiele możliwości jego dostosowania. Pełna lista znajduje się tutaj. Warto jeszcze wspomnieć:

  • interfaceOnly: Jeśli ustawimy tę wartość na true, wygenerowane zostaną tylko modele i interfejsy API, pozostawiając nam możliwość deklaracji własnych kontrolerów.
  • delegatePattern: jeśli ustawimy wartość true, generator utworzy dla nas interfejs delegata, który zostanie wstrzyknięty do kontrolera i wywołany przez niego.
  • useSpringBoot3: Określa, że powinien zostać wygenerowany kod zgodny ze Spring Boot 3, czyli użyta jakarta zamiast javax, etc.
  • openApiNullable: Określa, czy chcemy korzystać z funkcjonalności nullable openApi. Jeśli natomiast nie dodaliśmy zależności do niej, powinniśmy ustawić wartość „false”.

Wygenerowany kod

Po uruchomieniu polecenia mvn clean install możemy zobaczyć, że nasz kod został wygenerowany poprawnie:

Możemy teraz utworzyć serwis implementujący wygenerowany interfejs delegata, tak aby nasze endpointy zwracały coś innego niż status NOT_IMPLEMENTED.

@Slf4j
@Service
public class TaskHandler implements TaskApiDelegate {

    @Override
    public ResponseEntity<CreateTaskResponse> createTask(CreateTaskRequest createTaskRequest) {
        log.info("Handling create task request");
        return ResponseEntity.ok(new CreateTaskResponse(UUID.randomUUID().toString()));
    }

    @Override
    public ResponseEntity<List<Task>> getTasks(String name, Integer priority) {
        log.info("Handling get tasks request");
        return ResponseEntity.ok(List.of(new Task(UUID.randomUUID().toString(), "Task Name", 1)));
    }
}

Testowanie

Teraz nasza aplikacja poprawnie odpowiada na żądania:

Wnioski

Korzystanie z API-First z generatorem OpenAPI w Spring nie tylko pozwala nam oddzielić i udoskonalić logikę komunikacji za pomocą specyfikacji YAML, ułatwiając współpracę zespołu, ale także znacznie zmniejsza ilość czasu i energii, które musimy poświęcić na szablonowy kod. Dzięki prostej konfiguracji i wszechstronności generator OpenAPI może być świetnym dodatkiem do stosu technologicznego naszej aplikacji.

Bibliografia

Poznaj mageek of j‑labs i daj się zadziwić, jak może wyglądać praca z j‑People!

Skontaktuj się z nami