Podejście API-First wraz generatorem Open API

Filip Raszka

Dawniej podczas pisania aplikacji typu serwer-klient zwykle trzeba było wykonać pewną pracę wokół kontraktu API. Back-end i front-end uzgadniały wspólny kontrakt, a następnie obie strony implementowały swoje odpowiednie modele, usługi, kontrolery itp. W optymistycznym wariancie podczas integracji znajdowało się tylko kilka błędów, np. inaczej nazwane pola. W najgorszym przypadku całe sekcje kodu były zasadniczo niekompatybilne i wymagały dalszej pracy. Chociaż podejście API-First wraz z generatorami API nie usuwa całkowicie tych problemów, to znacznie je redukuje. Jak? 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, np. YAML ze specyfikacją OpenAPI, aby stworzyć wspólną specyfikację. Ta 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.

Jakie są 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

Spójrz na 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żesz 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ę znajdziesz 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 definiujesz swoje endpointy. Dla każdej ze ścieżek możesz zdefiniować wiele metod. Definicja metody może składać się z poniższych elementów.

  • Tag (logiczny kwalifikator 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 (np. zapytanie, ścieżka, nagłówki) mających zastosowanie dla danej operacji.
  • Opcjonalna treść żądania – treść żądania dla danej operacji.
  • Odpowiedź – lista odpowiedzi identyfikowanych przez status odpowiedzi http (mogą istnieć różne definicje dla różnych kodów). Dla określonego kodu statusu możesz 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żesz tu definiować obiekty wielokrotnego użytku, takie jak modele, do których możesz 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żesz zdefiniować obiekty OpenAPI w innych, osobnych plikach i połączyć je z głównym plikiem. W jaki sposób? Spójrz:

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

Jest to szczególnie przydatne, gdy Twoja specyfikacja API się rozrasta. 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 Open API Generator w Springu

Chcesz dodać generator OpenAPI do projektu Spring? Musisz dodać tylko jedną wtyczkę do konfiguracji Mavena 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 modele, interfejsy API i kontrolery. Wygenerowany kod będzie miał już walidację i adnotacje swaggera.

Konfiguracja

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

  • interfaceOnly – jeśli ustawisz tę wartość na true, wygenerowane zostaną tylko modele i interfejsy API, pozostawiając Ci możliwość deklaracji własnych kontrolerów.
  • delegatePattern – jeśli ustawisz wartość true, generator utworzy dla Ciebie 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.
  • openApiNullable – określa, czy chcesz korzystać z funkcjonalności nullable openApi. Jeśli natomiast nie dodasz zależności do niej, musisz ustawić wartość „false”.

Wygenerowany kod

Po uruchomieniu polecenia mvn clean install możesz zobaczyć, że Twój kod został wygenerowany poprawnie. Zobacz:

Możesz teraz utworzyć serwis implementujący wygenerowany interfejs delegata, tak aby Twoje 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 Twoja aplikacja poprawnie odpowiada na żądania. Spójrz:

Wnioski

Korzystanie z API-First z generatorem OpenAPI w Spring nie tylko pozwala oddzielić i udoskonalić logikę komunikacji za pomocą specyfikacji YAML, ułatwiając współpracę zespołu. Znacznie zmniejsza też ilość czasu i energii, które trzeba 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