Рассмотрим ситуацию:

  • В сервисе А есть метод для создания пользователя.

  • Метод принимает имя пользователя name, электронную почту email, пароль password. Поле name опционально, остальные - обязательные.

  • Приложение - клиент Б присылает запросы на создание пользователей, не указывая имя пользователя.

В определенный момент команда сервиса А изменяет протокол запроса создания пользователя - поле name становится обязательным. Через некоторое время приложение - клиент Б  присылает привычный запрос на создание пользователя, но получает ошибку валидации: “поле name должно быть заполнено”.

Описанный случай - это пример несогласованных протоколов общения. Команда сервиса А изменила протокол (поле name стало обязательным), но команда разработки приложения Б не была осведомлена об этом. При работе с микросервисной архитектурой эта проблема наиболее актуальна, потому что каждый микросервис может быть клиентом для любого другого микросервиса и разрабатываться отдельной командой. Для синхронизации протоколов общения и был создан подход к разработке серверных приложений API-First.

В статье рассматриваются особенности использования подхода API-First  на примере двух серверных java-приложений (одно приложение является клиентом другого) с использованием библиотек spring-boot, spring-web, openapi-generator, springdoc. Для описания спецификации API будет использован формат Openapi 3.*.

Что такое API-First

API-First - это подход к разработке серверных приложений, при котором API является наиболее важной частью продукта. При применении такого подхода у API вашего продукта есть собственный цикл разработки, соответственно создается его артефакт. Очевидно, что в вашем сервисе присутствует зависимость от артефакта API, чем обеспечивается актуальность спецификации в этом артефакте. Как видно, необходимо сначала разработать API, а потом уже работать над реализацией этого API в вашем сервисе. Указанная очередность разработки является необходимостью при приверженности к подходу API-First, но не определением самого подхода. First в наименовании  означает, что API - это первый по важности продукт.

У этого подхода имеются следующие преимущества:

  • Позволяет разрабатывать параллельно тесты, приложение-потребитель API и имплементацию API.

  • Не нужно предполагать, на каком языке будет реализовано приложение-клиент.

  • Появляется обязательный этап разработки API.

  • Спецификация API всегда актуальна.

  • Уменьшается риск возникновения различных ошибок при разработке.

  • Уменьшаются трудозатраты на разработку.

 

API-First и цикл разработки

При использовании микросервисной архитектуры почти любой сервис имеет API. Во время внедрения нового сервиса или  изменения API уже существующего сервиса важно, чтобы спецификация API удовлетворяла бизнес-требованиям. При классическом подходе (API после реализации, code-frist) это можно проверить только после реализации API. Если ошибка была найдена в спецификации, то исправления будут и в спецификации и имплеменатиции API. Команды разрабатывающие приложение-потребитель API и команда тестировния вынуждены ждать окончания работы над имплементацией API.

Иллюстрация классического подхода. Code First.

Долгого ожидания позволяет избежать подход API-First. После разработки API можно приступить к этапу внедрения API в приложение-потребитель и сервисе, реализующем API. Для наилучшего внедрения API предлагается использовать технику разработки TDD. На этапе разработки тестов можно проверить спецификацию API.

С этого момента начинается этап внедрения новой версии API. Для этого команда, создающая клиент, может разработать тесты разного уровня, в которых результат вызова API влияет на результат работы приложения-клиента. В тестах на данном этапе результат вызова API имитируется с помощью Mock'а.

Таким образом, автор тестов имитирует будущее использование API сервиса и получает возможность понять, удовлетворяет ли API требованиям, можно ли оптимизировать число вызовов сервиса, проверить состав передаваемой информации. Это является дополнительной проверкой спецификации API. Общую производительность команды это не ухудшит, а в случае нахождения ошибки даже улучшит.

Тесты могут быть написаны не только в приложении-потребителе API, но и в сервисе, реализующем API. Тесты в сервисе позволяют понять, реализуемо ли описанное в спецификации API: достаточно ли имеющихся и предоставляемых данных. Таким образом, ошибки проектирования API будут выявлены еще до его реализации. Это позволяет исправить основные ошибки дизайна API на этапе его разработки. API до имплементации является намного более гибким продуктом, чем сам сервис.

Частным случаем приложения-потребителя API является frontend. Соответственно, подход API-First позволяет frontend-команде также приступить к разработке сразу после выпуска финальной версии API.

После реализации API сразу появляется готовый стенд. Команде тестирования нужно будет лишь дождаться доставки сервиса с имплементацией на стенд.

Описанный подход позволяет всем командам-потребителям API работать параллельно. При этом ошибки спецификации API выявляются раньше, чем при стандартном подходе разработки (сначала реализовать API, потом отдать потребителю).

Изменения процесса разработки при использовании подхода API-First:

  1. Появляется возможность командам работать одновременно.

  2. Появляется отдельный продукт API, от которого зависит и сервис, и клиент этого сервиса. Соответственно, документация API не будет расходиться с его реализацией.

  3. Появляется этап внедрения API, что позволяет выявить его недостатки до его имплементации.

API-First на практике

Каждое приложение с API фактически будет состоять из двух проектов (как минимум) - API и его реализация.

При изменениях API сначала будет изменяться API-спецификация - SNAPSHOT API - отдельный артефакт. От него будет зависеть и сервис, реализующий API, и приложения-потребители.

После изменения версии артефакта API его могут забрать все команды, которые разрабатывают приложения-клиенты, и проверить, удовлетворяет ли команду созданное API. Одновременно с этим шагом команда сервиса, реализующего API, может по спецификации API начать писать его тесты.

Если команда приложения-потребителя и сервиса, реализующего API, убедилась в правильности спецификации, то создается release-артефакт API. После этого будет создана его реализация. Такой подход невозможен без выделения API в отдельный проект.

Для описания http API существует протокол OPENAPI 3.* . В статье рассматривается вариант использования этого протокола на примере springboot-приложений. Будет выполнено:

  1. Создание артефакта API.

  2. Генерация кода сервера: интерфейсов и моделей.

  3. Генерация кода клиента: моделей и Feign клиента.

Для генерации кода будет использована библиотека https://github.com/OpenAPITools/openapi-generator (gradle plugin).

Для визуализации спецификации API будет использоваться библиотека springdoc, использующая OPENAPI 3.*: https://springdoc.org/

В статье не будет подробно описан синтаксис OPENAPI 3.* и использование параметров плагина для генерации кода. Эти вещи достаточно хорошо задокументированы. Будут описаны лишь ключевые моменты их использования.

Рассмотрим пример из двух spring-boot-приложений (микросервисов):

  1. ApiFirstClient - серверное приложение, которое является клиентом ApiFirstServer https://github.com/SashaVolushkova/ApiFirstClient/tree/main 

  2. ApiFirstServer - серверное приложение https://github.com/SashaVolushkova/ApiFirstServer/tree/main 

Так как оба приложения - это приложения с http(s)-интерфейсом, у каждого из них есть свой проект API:

  1. ApiFirstMock https://github.com/SashaVolushkova/ApiFirstMock/tree/main  Openapi 3.0 спецификация API: https://github.com/SashaVolushkova/ApiFirstMock/blob/main/src/main/resources/petstore.yaml

  2. ApiFirstMockClient - https://github.com/SashaVolushkova/ApiFirstMockClient/tree/master Openapi 3.0 спецификация API: https://github.com/SashaVolushkova/ApiFirstMockClient/blob/master/src/main/resources/server.yaml 

Создание API-артефакта

В нашем примере за создание API-артефакта отвечают проекты ApiFirstMockClient и ApiFirstMock. Сборка артефакта API обязательно должна состоять из трех последовательных шагов:

  1. Валидации спецификации.

  2. Формирования артефакта.

  3. Публикации артефакта. 

Для создания API-проекта предлагаются следующие шаги:

https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator-gradle-plugin .

  • Добавить валидацию спецификации.

openApiValidate { 
  inputSpec = "${project.projectDir}/src/main/resources/petstore.yaml".toString() 
  recommend = true}
  • Добавить сборку артефакта в виде zip-файла со спецификацией.

task assembleArtifact(type: Zip, group: 'petstore-api') {
 	archiveName 'petstore-api.zip'
 	destinationDir file("$buildDir/libs/")
 	from "src/main/resources/petstore.yaml"
 	description "Assemble archive $archiveName into ${relativePath(destinationDir)}"
 }
  • Добавить зависимость сборки артефакта со спецификацией от валидации.

project.tasks.assembleArtifact.dependsOn('openApiValidate')
  • Добавить публикацию артефакта, созданного в task assembleArtifact.

publishing {
 	publications {
 		mavenJava(MavenPublication) {
 			artifact source: assembleArtifact, extension: 'zip'
 		}
 	}
 ….

}

В нашем примере рассматриваются два проекта API: ApiFirstMockClient и ApiFirstMock. Добавлено использование springdoc-openapi-ui (swagger-UI) для удобной визуализации спецификации. В общем случае проект API может состоять лишь из описания этапов валидации спецификации и сборки артефакта.

После сборки API-проектов в репозитории (для примера был использован nexus) должны появиться два API-артефакта, представляющие из себя zip-архив, в котором находится один файл. Они будут подключены как зависимости в наши приложения.

Создание сервера

Рассмотрим пример серверного приложения, в котором API используется в другом сервисе https://github.com/SashaVolushkova/ApiFirstServer .

Сборка проекта будет состоять из нескольких последовательных этапов:

  1. Взятие API-артефакта из репозитория.

  2. Распаковка API-артефакта.

  3. Генерация кода по API-спецификации.

  4. Подключение сгенерированного кода в src проекта. (может, в src-проект???)

Основные шаги

Рассмотрим один из способов реализации данных шагов. Весь сгенерированный код будет добавлен в папку build.

  1. Создать spring-boot gradle-проект со spring-boot-starter-web.

  2. Добавить плагин id "org.openapi.generator" version "6.1.0".

  3. Добавить gradle task.

  1. Добавить зависимость.

configurations {
 	openApiYaml { transitive = false }
 }
openApiYaml 'com.volushkova.apifirst:ApiFirstMock:0.0.1-SNAPSHOT'
  1. Добавить шаг распаковки зависимости.

task unzip(type: Copy) {
 	from zipTree(configurations.openApiYaml.singleFile).matching {
 		include 'petstore.yaml'
 	}
 	into "$buildDir/openapiDir"
 }
  1. Добавить описание sorceSet для добавления сгенерированного кода в src.

sourceSets {
 	main {
 		java {
 			srcDir "${buildDir}/openapiDir/generated/src/main/java/"
 		}
 	}
 }
  1. Добавить описание конфигурации для шага генерации кода из спецификации.

openApiGenerate {
 	generatorName = "spring"
 	outputDir = "${buildDir}/openapiDir/generated".toString()
 	inputSpec = "${buildDir}/openapiDir/petstore.yaml".toString()
 	globalProperties = [
 			apis: "",
 			models: "",
 			supportingFiles: 'ApiUtil.java'
 	]
 	apiPackage = "com.volushkova.apifirst.generated"
 	modelPackage = "com.volushkova.apifirst.generated.model"
 	skipOverwrite = true
 	configOptions = [
 			dateLibrary: "java8",
 			interfaceOnly: "true",
 			skipDefaultInterface: "false",
 			openApiNullable: "false",
 			generateSupportingFiles: "true",

                  library: "spring-boot"
 	]
 }

На этом шаге происходит самое главное - описание свойств сгенерированного кода. Некоторые важные параметры:

  1. generatorName - указание генератора кода. См. https://openapi-generator.tech/docs/generators/ 

  2. interfaceOnly - флаг, отвечающий за генерацию имплементации интерфейсов контроллера.

  3. skipDefaultInterface - флаг, отвечающий за добавление default-реализации интерфейсов. Если skipDefaultInterface = true, то будут добавлены default реализации методов. Возвращают статус 501 - not implemented.

  4. library - указание библиотеки для генератора. В нашем случае надо выбрать spring-boot. См. https://openapi-generator.tech/docs/generators/spring 

  5. Описать правильную очередность выполнения команд при сборке проекта.

project.tasks.openApiGenerate.dependsOn(unzip)
project.tasks.compileJava.dependsOn(project.tasks.openApiGenerate)

Итоговый результат

В сборку проекта теперь добавлен шаг openApiGenerate. По спецификации будут сгенерированы модели и интерфейсы. Далее можно будет эти интерфейсы использовать в основном коде для создания контроллеров.

Пример сгенерированной модели
/**
  * Error
  */
 
 @Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-09-13T17:33:33.802982300+03:00[Europe/Moscow]")
 public class Error {
 
   @JsonProperty("code")
   private Integer code;
 
   @JsonProperty("message")
   private String message;
 
   public Error code(Integer code) {
     this.code = code;
     return this;
   }
 
   /**
    * Get code
    * @return code
   */
   @NotNull 
   @Schema(name = "code", required = true)
   public Integer getCode() {
     return code;
   }
 
   public void setCode(Integer code) {
     this.code = code;
   }
 
   public Error message(String message) {
     this.message = message;
     return this;
   }
 
   /**
    * Get message
    * @return message
   */
   @NotNull 
   @Schema(name = "message", required = true)
   public String getMessage() {
     return message;
   }
 
   public void setMessage(String message) {
     this.message = message;
   }
 
   @Override
   public boolean equals(Object o) {
     if (this == o) {
       return true;
     }
     if (o == null || getClass() != o.getClass()) {
       return false;
     }
     Error error = (Error) o;
     return Objects.equals(this.code, error.code) &&
         Objects.equals(this.message, error.message);
   }
 
   @Override
   public int hashCode() {
     return Objects.hash(code, message);
   }
 
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
     sb.append("class Error {\n");
     sb.append("    code: ").append(toIndentedString(code)).append("\n");
     sb.append("    message: ").append(toIndentedString(message)).append("\n");
     sb.append("}");
     return sb.toString();
   }
 
   /**
    * Convert the given object to string with each line indented by 4 spaces
    * (except the first line).
    */
   private String toIndentedString(Object o) {
     if (o == null) {
       return "null";
     }
     return o.toString().replace("\n", "\n    ");
   }
 }
/**
  * Error
  */
 
 @Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-09-13T17:33:33.802982300+03:00[Europe/Moscow]")
 public class Error {
 
   @JsonProperty("code")
   private Integer code;
 
   @JsonProperty("message")
   private String message;
 
   public Error code(Integer code) {
     this.code = code;
     return this;
   }
 
   /**
    * Get code
    * @return code
   */
   @NotNull 
   @Schema(name = "code", required = true)
   public Integer getCode() {
     return code;
   }
 
   public void setCode(Integer code) {
     this.code = code;
   }
 
   public Error message(String message) {
     this.message = message;
     return this;
   }
 
   /**
    * Get message
    * @return message
   */
   @NotNull 
   @Schema(name = "message", required = true)
   public String getMessage() {
     return message;
   }
 
   public void setMessage(String message) {
     this.message = message;
   }
 
   @Override
   public boolean equals(Object o) {
     if (this == o) {
       return true;
     }
     if (o == null || getClass() != o.getClass()) {
       return false;
     }
     Error error = (Error) o;
     return Objects.equals(this.code, error.code) &&
         Objects.equals(this.message, error.message);
   }
 
   @Override
   public int hashCode() {
     return Objects.hash(code, message);
   }
 
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
     sb.append("class Error {\n");
     sb.append("    code: ").append(toIndentedString(code)).append("\n");
     sb.append("    message: ").append(toIndentedString(message)).append("\n");
     sb.append("}");
     return sb.toString();
   }
 
   /**
    * Convert the given object to string with each line indented by 4 spaces
    * (except the first line).
    */
   private String toIndentedString(Object o) {
     if (o == null) {
       return "null";
     }
     return o.toString().replace("\n", "\n    ");
   }
 }

Пример интерфейса
@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-09-13T17:33:33.802982300+03:00[Europe/Moscow]")
 @Validated
 @Tag(name = "pets", description = "the pets API")
 @RequestMapping("${openapi.exampleOfApiForGenerate.base-path:}")
 public interface PetsApi {
 
     default Optional<NativeWebRequest> getRequest() {
         return Optional.empty();
     }
 
     /**
      * POST /pets : Create a pet
      *
      * @return Null response (status code 201)
      *         or unexpected error (status code 200)
      */
     @Operation(
         operationId = "createPets",
         summary = "Create a pet",
         tags = { "pets" },
         responses = {
             @ApiResponse(responseCode = "201", description = "Null response"),
             @ApiResponse(responseCode = "200", description = "unexpected error", content = {
                 @Content(mediaType = "application/json", schema = @Schema(implementation = Error.class))
             })
         }
     )
     @RequestMapping(
         method = RequestMethod.POST,
         value = "/pets",
         produces = { "application/json" }
     )
     default ResponseEntity<Void> createPets(
         
     ) {
         return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
 
     }
 
 
     /**
      * GET /pets : List all pets
      *
      * @param limit How many items to return at one time (max 100) (optional)
      * @return A paged array of pets (status code 200)
      *         or unexpected error (status code 200)
      */
     @Operation(
         operationId = "listPets",
         summary = "List all pets",
         tags = { "pets" },
         responses = {
             @ApiResponse(responseCode = "200", description = "A paged array of pets", content = {
                 @Content(mediaType = "application/json", schema = @Schema(implementation = Pet.class))
             }),
             @ApiResponse(responseCode = "200", description = "unexpected error", content = {
                 @Content(mediaType = "application/json", schema = @Schema(implementation = Error.class))
             })
         }
     )
     @RequestMapping(
         method = RequestMethod.GET,
         value = "/pets",
         produces = { "application/json" }
     )
     default ResponseEntity<List<Pet>> listPets(
         @Parameter(name = "limit", description = "How many items to return at one time (max 100)") @Valid @RequestParam(value = "limit", required = false) Integer limit
     ) {
         getRequest().ifPresent(request -> {
             for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                 if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                     String exampleString = "{ \"name\" : \"name\", \"id\" : 0, \"tag\" : \"tag\" }";
                     ApiUtil.setExampleResponse(request, "application/json", exampleString);
                     break;
                 }
             }
         });
         return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
 
     }
 
 
     /**
      * GET /pets/{petId} : Info for a specific pet
      *
      * @param petId The id of the pet to retrieve (required)
      * @return Expected response to a valid request (status code 200)
      *         or unexpected error (status code 200)
      */
     @Operation(
         operationId = "showPetById",
         summary = "Info for a specific pet",
         tags = { "pets" },
         responses = {
             @ApiResponse(responseCode = "200", description = "Expected response to a valid request", content = {
                 @Content(mediaType = "application/json", schema = @Schema(implementation = Pet.class))
             }),
             @ApiResponse(responseCode = "200", description = "unexpected error", content = {
                 @Content(mediaType = "application/json", schema = @Schema(implementation = Error.class))
             })
         }
     )
     @RequestMapping(
         method = RequestMethod.GET,
         value = "/pets/{petId}",
         produces = { "application/json" }
     )
     default ResponseEntity<Pet> showPetById(
         @Parameter(name = "petId", description = "The id of the pet to retrieve", required = true) @PathVariable("petId") String petId
     ) {
         getRequest().ifPresent(request -> {
             for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                 if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                     String exampleString = "{ \"name\" : \"name\", \"id\" : 0, \"tag\" : \"tag\" }";
                     ApiUtil.setExampleResponse(request, "application/json", exampleString);
                     break;
                 }
             }
         });
         return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
 
     }
 
 }

Имплементация интерфейса выглядит следующим образом:

@Controller
 public class PetApiImpl implements PetsApi {
     private List<Pet> pets = new ArrayList<>();
     @Override
     public ResponseEntity<Void> createPets() {
         // your future implementation
         Pet pet1 = new Pet();
         pet1.setId(1L);
         pet1.setName("cat");
         pet1.setTag("cat");
         pets.add(pet1);
         return ResponseEntity.status(HttpStatus.CREATED).build();
     }
 
     @Override
     public ResponseEntity<List<Pet>> listPets(Integer limit) {
         // your future implementation
         return ResponseEntity.ok(pets);
     }
 }

Создание клиента

В нашем случае клиент - это тоже серверное приложение, поэтому пропустим описания шагов по генерации кода сервера.

Основные шаги сборки:

  1. Взятие API-артефактов из репозитория - API клиента, API сервера.

  2. Распаковка API-артефактов.

  3. Генерация кода по API-спецификациям:

  4. Генерация кода клиента.

  5. Генерация кода сервера.

  6. Подключение сгенерированного кода в src проекта.

Чтобы добавить генерацию из нескольких файлов с Openapi-спецификацией, нужно воспользоваться  конструкцией:

import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
 tasks.register("petstoreClient", GenerateTask) {

   ….

}

Обе задачи по генерации кода нужно выставить вперед компиляции, как и в  предыдущем примере.

В приложении-клиенте будет использоваться генератор младшей версии. На момент написания статьи id "org.openapi.generator" version "6.1.0" имел баг: неверно генерировался интерфейс контроллера. Его нельзя было применить для создания Feign клиента из-за лишней аннотации @RequestMapping.  В версии генератора 5.4.0 и  7.0.0-SNAPSHOT такого бага не обнаружено.

Генерация Feign клиента

Для генерации кода клиента необходимо описать параметры соответствующей задачи. В ходе генерации будут созданы модели и Feign клиент для общения с сервисом. Для генерации Feign клиента нужно воспользоваться spring с указание library spring-cloud.

tasks.register("petstoreClient", GenerateTask) {
 	generatorName = "spring"
 	outputDir = "${buildDir}/openapiDir/generated".toString()
 	inputSpec = "${buildDir}/openapiDir/petstore.yaml".toString()
 	globalProperties = [
 			apis: "",
 			models: "",
 			supportingFiles: "ClientConfiguration.java"
 	]
 	apiPackage = "com.volushkova.apifirst.generated.petclient"
 	modelPackage = "com.volushkova.apifirst.generated.petclient.model"
 	skipOverwrite = true
 	configOptions = [
 			dateLibrary: "java8",
 			openApiNullable: "false",
 			library: "spring-cloud"
 	]
 
 }

Итоговый результат

Созданы модели для сервера и для клиента. Создан Feign клиент, который можно использовать для общения с другим сервисом.

Пример Feign сгенерированного клиента:

@FeignClient(name="${pets.name:pets}", url="${pets.url:http://localhost:8080}", configuration = ClientConfiguration.class)
 public interface PetsApiClient extends PetsApi {
 }

Заключение

В статье рассмотрен проект с использованием спецификации в качестве API-артефакта. При таком подходе применяются все принципы API-First и используются все его преимущества:

  • Не нужно думать о том, кто будет использовать ваше API и какой язык будет использовать другая команда разработки. Библиотека openapi-generator предоставляет множество языков для генерации как клиентского, так и серверного кода.

  • Появляется больше возможностей для параллельной разработки. После готовности API команды тестирования, разработки клиента API и разработки сервиса, реализующего API, могут рабоать одновременно, что приводит к увеличению производительности команд.

  • При таком подходе есть обязательный этап описания API, в котором можно и нужно требовать качественное описание методов и моделей.

  • За счет генерации кода из спецификации уменьшается объем рутинного кода. Появляется удобное визуальное представление (swagger-ui) для чтения документации вашего API. Документация API не будет расходиться с кодом, ведь именно по ней генерируется код сервиса. По этой же причине спецификация (за которую отвечает команда сервиса) будет всегда актуальной и ее смело смогут использовать приложения-клиенты вашего сервиса.

  • За счет применения TDD и ранней работы с API до его реализации ошибки дизайна API выявляются раньше, чем при стандартном подходе разработки (API после реализации). Таким образом уменьшается риск при разработке, связанный с ошибками документацией.

Автор: Александра Волушкова aka SashaVolushkova - Java Backend Developer / Хабр (habr.com)

Комментарии (6)


  1. lebedec
    20.10.2022 13:04
    +1

    Ирония в том, что этот подход не что иное, как воплощение классических этапов разработки, о которых всё время твердят олды, но шарахаются Agile адепты: проектирование и системный анализ.

    На практике именно так. Четкая спецификация API позволяет бустануть разработку в разы, автоматизировать некоторую деятельность, увеличить качество и стабильность софта.

    Я бы еще добавил, что наличие генерируемых интерфейсов для сервера не гарантирует вам что они будут реализованы. Поэтому на сервере можно добавить механизм, который будет явно верифицировать спеку и реализованный бэкенд.

    Любой API фреймворк фактически регистрирует все модели и роуты, достаточно привести это к формату вашей спеки и сравнить. Так на продакшене точно не окажется сервис, который реализует заявленный API не полностью.

    Это особенно актуально для динамических языков типа Python.


    1. SashaVolushkova
      20.10.2022 14:06

      Не совсем. Agile не отрицает написание документации перед выполнением самой задачи. Как раз при использовании agile фреймворков и возникают разлады с несогласованными протоколами общения из-за частых изменений и релизов. Подход как и раз и позволяет держать единый формат спецификации.


      1. lebedec
        20.10.2022 14:51

        Не отрицает, я под Agile адептами имел в виду некоторый собирательный образ мейнстримового IT менеджера, для которого проектирование API звучит как саботаж эффективной работы, как призыв к "устаревшим" моделям управления разработкой.

        А по сути нет никакого "API-First" подхода. Есть рациональный и давно известный подход — сначала анализируешь, проектируешь, затем реализуешь. И это даёт все перечисленные в статье преимущества.

        Просто в один момент отрасль поразила идея, что для эффективной разработки можно только "обсудить на митинге", а всё остально табу. Вот и приходится уже существующие практики и опыт преподносить по новому, придумывать новые названия, лишь бы не пугать тех самых менеджеров.


        1. SashaVolushkova
          20.10.2022 16:55

          В плане проектирования, да Вы правы, ничего нового. Но если посмотреть с точки зрения разработчика, то весьма редко начинают программировать сервис именно с API. В итоге много ожиданий и неожиданных находок, когда уже, казалось бы, всё готово.


    1. SashaVolushkova
      20.10.2022 14:31

      Конечно, использование подхода не гарантирует наличие имплементации. Это гарантирует процесс тестирования (который в этом случае начинается рано - сразу после разработки спецификации). Если уж на продакшн попал не реализованный метод API, то скорее всего, он мало приоритетный. На мой взгляд, это всё равно лучше, чем если бы на продакшн сервисе оказался неверно имплементированный метод или несовпадение со спецификацией.


      1. lebedec
        20.10.2022 14:55

        На мой взгляд, это всё равно лучше, чем если бы на продакшн сервисе оказался неверно имплементированный метод или несовпадение со спецификацией.

        Так я же не отрицаю. Это я к тому, что не все языки как Java умеют гарантировать соответствия интерфейса реализации. Поэтому, если надо, можно использовать формальность API спеки и реализовать проверку в более общем виде. Это еще одно преимущество API-First.