OpenAPI стал одним из стандартов для описания API с большим числом сопутствующих инструментов и сервисов. По сути, OpenAPI — это спецификация на составление спецификаций, поэтому она описывает множество вариантов взаимодействия. Но как лучше всего проектировать API, используя OpenAPI? У команды Sape есть многолетний опыт в этой сфере, о котором я сейчас расскажу.

Сразу определимся — о каком применении OpenAPI идёт речь. OpenAPI формализует самые различные взаимодействия в форматах JSON / YAML: тело запроса может быть как классической HTML-формой, так и URL-кодированным GET-запросом или XML. Есть возможность описать параметры как часть пути URL-обращения, как GET-параметры или как передаваемые в теле запроса. Есть различные способы кодирования параметров внутри этих логических частей запроса. Иными словами, OpenAPI позволяет описать большинство уже существующих RESTful API. Это очень удобно, но для конкретного применения — если мы создаём спецификацию с нуля — нам нужно определиться с договорённостями. В нашем случае это:

  • формат передачи данных – JSON;

  • основной потребитель API – SPA;

  • типы ответов универсальны для всех API-методов;

  • используем подход Manifest First;

  • в проектировании мы используем Domain-driven design.

Потребители API

Создавая метод в API, нужно задаваться вопросом: “Как этим будут пользоваться внешние потребители?” Перечислю потребителей для нашего гипотетического API:

  • фронтенд-приложение Single Page Application;

  • внешние пользователи, использующие наш API;

  • Пользователи через инструмент автодокументации.

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

Подход Manifest First

Манифест — это соглашение по взаимодействию. В случае OpenAPI мы можем описать это взаимодействие таким образом, что оно не будет завязано на конкретный язык программирования или фреймворк, будет понятно программистам и менеджерам. Одно из преимуществ такого соглашения заключается в том, что составление манифеста относительно дёшево (легко научиться, быстро написать).

Мы используем подход Manifest First — первым создаётся манифест (в терминологии OpenAPI — спецификация). Он является источником истины для приложения. Относительно него реализуются функции фронтенда и бэкенда.

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

Этапы разработки, выработанные в нашей компании:

  1. Предоставление бизнес-требований в виде макетов в Figma и сопроводительной документации, описывающей логику работы UI.

  2. Составление по ним спецификации.

  3. Вёрстка макетов без интеграции с API.

  4. Параллельная разработка фронтенда и бэкенда.

  5. Финализация фронтенда для проверки взаимодействия с бэкендом.

  6. Проведение через QA на стейджинге.

  7. Релиз зоны или приложения.

Процесс выпуска новых зон приложения
Процесс выпуска новых зон приложения

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

Как показывает наш опыт, составление спецификации и вёрстка для SPA — относительно быстрые этапы. Для простых сервисов её можно составить за пару часов. Современная вёрстка — также хорошо организованный процесс, если имеется хорошо оформленный макет в таких системах как Figma. Наиболее объёмные по трудоёмкости этапы разработки — это реализация со стороны фронтенда и бэкенда. Однако они распараллеливаются, что даёт отличный выигрыш по времени. Применяя подход Manifest First, пользователи получают новые фичи значительнее быстрее, чем при традиционной разработке.

Имитация фронтенда и бэкенда

С чем же связана возможность по распараллеливанию реализаций фронтенд / бэкенд? Не только с тем фактом, что есть готовая спецификация и можно попробовать писать “под неё”, представляя себе, как будет себя вести другая сторона взаимодействия по API. Формализация соглашений по взаимодействию позволяет создать имитаторы этой “другой стороны”.

На сайте OpenAPI.Tools в разделе “Mock Servers” можно найти примеры таких имитаторов. Эти программы принимают на вход спецификацию и динамически создают ответы на них в соответствии с ней. Имитаторы могут использовать секции “example” из спецификации, чтобы формировать структуру ответа и подставлять оттуда конкретные значения или самостоятельно придумывать разнообразные значения, исходя из определений типов данных.

Мы активно используем API Sprout, который использует только данные из example, однако, он очень удобен благодаря отслеживанию изменений в спецификации (режим наблюдения — --watch), валидации запросов и ответов (--validate-request / --validate-server), и автоматической простановке CORS-заголовков. Также пробовали Fakeit, но он оказался не совсем удобен из-за того, что требует перезапуска при изменении спецификации. Зато он рандомизирует ответы, что делает работу с ним довольно весёлым занятием — каждый раз в UI отображается что-нибудь новое.

OpenAPI допускает задание примеров значений у свойств объектов через example. Это свойство и его использование в имитаторах бэкенда для подстановки очень помогает фронтенду тестировать получающиеся интерфейсы. Фронтендеры могут через example формировать разные случаи. Например, разные статусы у сущностей в списках или разные ответы API. Можно задать, например, очень длинные строки в названиях, чтобы увидеть, не нарушилась ли от этого вёрстка. Манипулируя с локальной копией спецификации фронтендеры могут, по сути, проводить первичный этап QA.

Со стороны фронтенда также есть своего рода имитаторы. Самый популярный из них — Swagger UI. Это — небольшое фронтендное приложение, которое запущено на сервере создателей OpenAPI — компании Swagger, но ничто не мешает запустить его локально. В приложении по URL загружается спецификация, после чего появляется автодокументация с возможностью интерактивного использования:

Интерфейс Swagger UI
Интерфейс Swagger UI

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

Для отладки OpenAPI-запросов незаменим Postman. Этот инструмент гораздо шире, чем просто среда для формирования запросов через OpenAPI и проверки результатов. Он поддерживает и другие виды API, а также имеет встроенный средства для написания автотестов. Он, как и Swagger UI, умеет формировать структуры запросов в соответствии со спецификацией. Также он умеет валидировать ответы, находя в структуре различия с заявленной в спецификации.

Интерфейс Postman
Интерфейс Postman

Как OpenAPI пришёл в Links.Sape

Наша команда начала разрабатывать приложения на базе OpenAPI в 2019 году. Первым был один из сервисов, который в настоящий момент заморожен. Поскольку это был первый опыт применения технологии в production, автоматизации было немного, инструменты для работы были только внешними. Мы составляли спецификацию, затем писали классы запросов и ответов, а также валидаторы для форм запросов. Это требовало большой сосредоточенности и механической работы, поэтому было чревато ошибками. Впоследствии, когда мы убедились, что технология оправдала себя, был создан инструмент автоматизации — клиентский и серверный генератор бэкенда для OpenAPI (для простоты буду его называть “OpenAPI-генератор”).

OpenAPI-генератор открыл в нашей компании новую эру в разработке. Благодаря ему мы сумели запустить новый интерфейс Links.Sape. Если кратко, генератор — это Symfony-бандл, который на основе заданной спецификации генерирует в приложении (в случае генерации серверной части):

  • маршруты (routes);

  • контроллеры (controllers);

  • заглушки для API-сервисов (service stubs);

  • DTO для запросов и ответов (ApiRequests / ApiResponses);

  • вспомогательные формы запросов для их валидации (Request Forms).

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

Из чего состоит спецификация

Технически спецификация — это файл в формате JSON, состоящий из логических частей:

  • свойства “openapi” с указанием версии используемой спецификации;

  • секции “info” с описанием данного API (название, версия и пр.);

  • секция серверов — куда идут обращения (начальные пути URL серверов API);

  • секция тегов — нужны для маркировки путей обращения, но используется нами как список сущностей;

  • список путей —точки обращения (endpoints) API, к которым идёт обращение (содержат в себе запрос и возможные ответы);

  • компоненты — схемы и параметры, совместно используемые в endpoint’ах;

  • другое: схема безопасности и т.п.

Допустима спецификация в YAML, и Swagger по умолчанию использует его. Но в качестве формата передачи данных всё же привычнее JSON.

Полная спецификация на спецификации находится тут. В качестве примера можно взять спецификацию нового интерфейса.

Теперь разберём каждую из секций файла спецификации.

Секция OpenAPI: серверы (servers)

Указывает перечень начальных URL для обращения к API. Обычно это — схема и домен сервера. Например, https://api-links.sape.ru. Разработчики могут указать в качестве сервера development, staging-площадку или URL имитатора бэкенда.

 "servers": [
    {
      "url": "https://api-links.sape.ru"
    },
    {
      "url": "https://apisprout.sape.ru"
    },
    {
      "url": "http://localhost:3333"
    }
  ]

По умолчанию клиенты используют первый указанный сервер. Swagger UI позволяет выбирать сервер обращения из списка в своём UI:

Выбор сервера для обращения в Swagger UI
Выбор сервера для обращения в Swagger UI

Секция OpenAPI: теги (tags) 

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

Именование: CamelCase. Как это принято в OpenAPI, сущность называем во множественном числе (если нет противоречия в контексте — к примеру, системная конфигурация единственная).

 "tags": [
    {
      "name": "Autobuyers",
      "description": "Авторежимы"
    },
    {
      "name": "BlackLists",
      "description": "Чёрные списки"
    },
    {
      "name": "GlobalBlackLists",
      "description": "Глобальные чёрные списки"
    },
    {
      "name": "Keywords",
      "description": "Ключевые слова"
    },
    {
      "name": "FavoriteLists",
      "description": "Списки избранных сайтов"
    }
…
]

Теги нам в дальнейшем понадобятся для удобных ссылок на методы API.

Секция OpenAPI: пути (paths)

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

Рассмотрим пример:

   "/rest/Autobuyer/rent/{autobuyerId}": {
      "get": {
        "tags": [
          "Autobuyers"
        ],
        "summary": "Просмотр арендного авторежима",
        "operationId": "getAutobuyerRent",
        "parameters": [
          {
            "$ref": "#/components/parameters/autobuyerId"
          }
        ],
        "responses": {
          "200": {
            "description": "Просмотр данных арендного авторежима",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AutobuyerRent"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ResponseBadParameters"
          },
          "401": {
            "$ref": "#/components/responses/ResponseUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ResponsePermissionDenied"
          },
          "404": {
            "$ref": "#/components/responses/ResponseNotFound"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ]
      },
…
  • "/rest/Autobuyer/rent/{autobuyerId}" — URL обращения (относительно выбранного сервера) с подстановкой параметра как часть URL;

  • “get” — HTTP-метод обращения: GET, POST, PUT, PATCH, DELETE.
    Выбирается семантически (см. статью Use HTTP Methods Correctly);

  • “summary” — описание для автодокументации;

  • “operationId” — уникальный идентификатор операции, используемый в Tags Interface.
    Чтобы сослаться на API-метод в документации, мы используем Tags Interface: название тега + ID операции. В данном случае это Autobuyers.getAutobuyerRent(). Такую нотацию использует, в частности JavaScript-клиент для OpenAPI — swagger-js.
    operationId формируется из глагола действия (add — добавить, update — обновить и т.д.) + название сущности (Project, User, Autobuyer, Text и т.п.) + опционально вспомогательная информация (тут уже свободная форма: List — список, Brief — для краткого списка и т.п.)

  • "parameters" — массив параметров, использованных как часть URL или в виде GET-параметров. Вид подстановки определяется описанием компонента в схеме, об этом ниже.

  • "responses" — массив возможных ответов. Ключ — код ответа.

  • "security" — требования по безопасности.

Соглашение по именованию свойств: lowerCamelCase. Компонентов (поскольку они объекты) — CamelCase.

Секция OpenAPI: компоненты (components)

Описывает переиспользуемые структуры: параметры запросов (parameters) и схемы (schemas). При создании спецификации нужно стараться переиспользовать параметры, используя возможность ссылок в OpenAPI — $ref. $ref ссылается на секцию компонентов, где, в частности, описываются параметры.

Параметры запроса могут содержаться в секции “parameters” секции “paths” для любых HTTP-методов, а также в “requestBody” для методов, имеющих тело запроса (POST, PATCH, PUT).

В примере выше мы таким же образом используем структуры из секции компонентов:

  • Параметр autobuyerId из URL обращения — #/components/parameters/autobuyerId.

  • Сущность арендного авторежима — #/components/schemas/AutobuyerRent.

  • Объекты ответов: #/components/schemas/AutobuyerRent, #/components/responses/ResponseBadParameters и др.

Давайте посмотрим на примере, как следует читать ссылки на параметры. 

Предположим, описана точка обращения в секции “paths”:

"/rest/Autobuyer/rent/{autobuyerId}": {
   "get": {
…

Описание параметра находим ниже, в секции “parameters” самой точки обращения:

"parameters": [
  {
    "$ref": "#/components/parameters/autobuyerId"
  }
],

В свою очередь, #/components/parameters/autobuyerId ссылается на:

     "autobuyerId": {
        "name": "autobuyerId",
        "in": "path",
        "description": "ID авторежима",
        "required": true,
        "example": 3643,
        "schema": {
          "type": "integer",
          "format": "int32",
          "minimum": 1
        }
      },

В PhpStorm с установленным плагином “OpenApi Specifications” достаточно нажать Ctrl+b, чтобы перейти к определению.

Здесь мы видим описание параметра. У него определено имя параметра (то, что используется в описании точки обращения), “in” — определяет, что это именно часть пути обращения, “description” — описание для человека, “required” — что параметр обязателен, “example” — пример для имитатора запросов, “schema” — описание типа данных (число разрядностью int32, начиная с 1).

Аналогично в компонентах спецификации описываются сущности предметной области нашего приложения и другие переиспользуемые объекты.

Например, в описании запроса как на получение арендного авторежима (Autobuyers.getAutobuyerRent()), так и на его обновление (Autobuyers.updateAutobuyerRent()) мы используем ссылку на одну и ту же схему — "#/components/schemas/AutobuyerRent", которая выглядит так (в сокращении):

     "AutobuyerRent": {
        "type": "object",
        "title": "Арендный авторежим",
        "properties": {
          "autobuyerCommon": {
            "$ref": "#/components/schemas/Autobuyer"
          },
          "searchFilterId": {
            "type": "integer",
            "title": "ID арендного авторежима",
            "example": 101
          },
          "urls": {
            "title": "URLы авторежима",
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AutobuyerRentUrl"
            }
          },
          "counters": {
            "type": "object",
            "title": "Счётчики",
            "properties": {
              "nofLinksStatusPlaced": {
                "type": "integer",
                "title": "Число размещенных ссылок",
                "minimum": 0
              },
              "nofLinksStatusInProcess": {
                "type": "integer",
                "title": "Число ссылок в процессе размещения",
                "minimum": 0
              },
              "amountOfBoughtLinks": {
                "type": "number",
                "format": "float",
                "title": "Куплено ссылок на сумму",
                "minimum": 0
              }
            },
            "readOnly": true
          }
        }
      },

Это — объёкт (“type”: “object”), имеющий свойства (“properties”) различного типа, в т.ч. другие сущности. В данном случае Арендный авторежим — один из подтипов более общего Авторежима. Поэтому при при чтении нам нужно получить общие базовые авторежимные данные (свойство autobuyerCommon).

Контекст использования сущности

Очевидно, что при получении и обновлении сущности используется разный набор свойств. Для дифференциации контекста используется свойство “readOnly”. Если оно true, то поле не будет приниматься на запись — клиент не будет его видеть в списке доступных полей, к примеру, в Autobuyers.updateAutobuyerRent() не будет поля “id” и “projectId”.

     "Autobuyer": {
        "type": "object",
        "title": "Авторежим",
        "properties": {
          "id": {
            "type": "integer",
            "title": "ID авторежима",
            "example": 10,
            "readOnly": true
          },
          "projectId": {
            "type": "integer",
            "title": "ID проекта",
            "readOnly": true,
            "example": 125
          },
          "name": {
            "type": "string",
            "title": "Имя авторежима",
            "minLength": 3,
            "example": "Успешный авторежим"
          },
          "type": {
            "type": "integer",
            "enum": [1, 10],
            "title": "Тип авторежима",
            "description": "Типы авторежимов: 1 - арендный, 10 - статейный",
            "readOnly": true,
            "example": 1
          },
…

Autobuyers.getAutobuyerRent() в Swagger UI (контекст чтения):

Autobuyers.updateAutobuyerRent() в Swagger UI (контекст записи):

При определении схем также важно как можно более детально описывать ограничения на поля. Кроме типов можно задать минимальную и максимальную длину строки: minLength / maxLength; для чисел — минимальное и максимальное значение: minimum / maximum; для любого типа можно задать перечень допустимых значений через enum; возможно задание nullable для полей, принимающих значение null.

Эти свойства могут использоваться в преобразования валидаторов формы запроса. OpenAPI-генератор, используемый в Sape, именно так и делает. Продвинутые имитаторы бэкенда также умеют валидировать как запросы, так и ответы сервера на основе этих ограничений.

OpenAPI: ответы (responses)

Секция описывает возможные ответы на запрос к API. Ключ — HTTP-код ответа. Пример:

       "responses": {
          "200": {
            "$ref": "#/components/responses/ResponseAction"
          },
          "400": {
            "$ref": "#/components/responses/ResponseBadParameters"
          },
          "401": {
            "$ref": "#/components/responses/ResponseUnauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ResponsePermissionDenied"
          },
          "404": {
            "$ref": "#/components/responses/ResponseNotFound"
          }
        },

Мы используем несколько стандартных для приложения типов ответов. Кому интересно, можно почитать в нашей спецификации.

Тип ответа 200 может быть как стандартным (условное “действие совершено”), так и переопределенным. Остальные коды имеет смысла всегда делать одинаковыми, стандартными.

Фреймворк совместно с OpenAPI-генератором задают структуру, в том числе ответов — преобразуя исключения в нужные типы ответов. В частности, код 400 применяется для отображения ошибки валидации формы запроса. Компонент OpenAPI ResponseBadParameters содержит в себе список полей и ошибок к ним:

     "ResponseBadParameters": {
        "description": "Заданы неверные параметры",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "title": "Общая ошибка",
              "properties": {
                "message": {
                  "type": "string",
                  "title": "Сообщение, описывающее ошибку"
                },
                "errorSchema": {
                  "type": "array",
                  "title": "Ошибки в параметрах запроса",
                  "description": "Массив имён ошибочно заполненных полей и соответствующих им текстам ошибок",
                  "items": {
                    "type": "object",
                    "properties": {
                      "name": {
                        "type": "string",
                        "pattern": "[a-zA-Z0-9_\\[\\]]+",
                        "title": "Имя поля"
                      },
                      "errorMessage": {
                        "type": "string",
                        "title": "Текст ошибки для данного поля"
                      }
                    }
                  }
                }
              },
              "required": [
                "message"
              ]
            }
          }
        }
      },

SPA нового интерфейса имеет интеграцию с этими кодами для обобщения поведения приложения: переадресаций на ошибочные страницы (например, код 401), подсветку ошибок формы, вывод сообщений об успешных операциях и т.п.

Например, мы создаём проект и задали несуществующий домен для продвижения. API-метод Projects.addProject() ответил 400-ым кодом с таким телом:

errorSchema: 
  0:
    errorMessage:  "Домен не найден в списке зон"
    name: "domain"
message: ""

В данном случае общей ошибки у нас нет (поле message не заполнено), но в интерфейсе мы увидели эту ошибку, привязанную к полю domain формы (она оказалась в errorSchema):

Такая схема ответов оказалась очень удачной. Обработка параметров запроса соответствует тому, что пользователь видит в UI.

Методика проектирования

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

Итак, перед нами макет страницы или одна из зон нового приложения. Поскольку удобнее сразу проектировать базу данных, заводим два списка: список сущностей, их свойств и методов OpenAPI и список сущностей и свойства в БД. Попутно благодаря такому сопоставления можно увидеть узкие места и взаимно учесть их.

Алгоритм такой. Проходим по элементам интерфейса на макете и пополняем оба списка. Необходимо для каждого элемента UI задаться вопросами:

  1. К какой сущности относится элемент? Если её ещё нет, вносим в оба списка.

  2. В случае неактивного элемента — что в нём отображается? В обоих списках добавляем свойства. Задумываемся сразу о денормализации: выносим в таблицу счётчиков предвычисляемые данные.

  3. Для БД разбиваем данные на связанные сущности: счётчики, расширенные данные и т.п. Удобно накидывать такие структуры сразу в процессе изучения макета, чтобы понимать, что нам потребуется.

  4. В случае action-элемента — что происходит при нажатии? Требуется ли подтверждение (например, диалоговое окно)? Как изменяется состояние приложения? Фиксируем operationId. Продумываем, исходя из результирующего действия, какие нужны данные (например, ID созданной сущности для последующего перехода). Или, к примеру, бизнес-логика предусматривает переход на диалог пополнения счёта в случае нехватки средств. В таком случае потребуется вернуть специальный флажок, говорящий о недостаточном балансе.

  5. Какие могут возникнуть ошибки при обработке? Какие поля формы обязательные?

  6. Для нужд вёрстки также: title для элемента? Как будут отработаны ошибочные ситуации?

Этапы отображения интерфейса

Мы разрабатываем SPA, поэтому нужно учитывать особенности динамических интерфейсов. Наше приложение, скорее всего, не будет загружаться одномоментно. Чтобы пользователь получил необходимое как можно скорее, а система при этом не перегружалась, мы можем разделить отображение на несколько этапов.

Перед нами макет страницы проекта:

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

Далее пользователю важно увидеть список URL и базовую информацию по ним, а также счётчики сущностей в них.

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

Типы зон

Работая с зонами приложения, мы имеем дело со следующими типами страниц и операций:

  • Добавление / создание. Тут дополнительные данные не нужны. Это — POST-метод. Например, Autobuyers.createAutobuyerRent(), Projects.addProject().

  • Обновление. Логически состоит из получения данных для отображения и записи. Данные для отображения формы получаем через GET-метод (например, Projects.getProject()). Также нужен PATCH-метод непосредственно обновления (например, Projects.updateProject()).

  • Список сущностей. GET / POST-метод, в зависимости от наличия объёмных параметров, таких как сложные фильтры, потенциально не умещаемых в GET-запрос. Например, ProjectGroups.getProjectsGroups() возвращает список Групп проектов. Учитываем получение данных для вторичных этапов отображения. Например, кроме списка проектов, Projects.getProjectsList(), понадобится также данные для построения графиков по ссылочному профилю, Projects.getProjectsGraphics().

  • Специализированные операции в любой из зон. Удаление сущности, используем HTTP-метод DELETE. Ещё примеры: быстрый поиск по названию, покупка, архивация и т.п.

На что ещё обратить внимание

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

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

Желательно переиспользование одни и тех же методы в нескольких местах UI. С этой целью имеет смысл делать универсальные методы, если это не влияет принципиально на трудоёмкость реализации или ресурсоёмкость на бэкенду. В то же время, для увеличения отзывчивости UI имеет смысл и создание “легковесных” методов. В целом нужно искать баланс: с одной стороны узкая специальность и легковесность API-метода, с другой — переиспользуемость и универсальность. В этом, на мой взгляд, и заключается искусство проектирования API. Этот баланс лучше всего находить совместно фронтенду с бэкендом.

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

Начинаем проектировать: создание проекта

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

Перед нами сущность Проект. Ранее она нам не встречалась, поэтому внесём её в оба списка и присвоем ей название Project. Видим поля формы, которые мы трактуем как поля сущности в OpenAPI и как поля в базе данных: “домен” (domain), “название” (name), “группа”...

Останавливаемся и выясняем из бизнес-логики, что речь идёт о сущности Группа проекта (присваиваем название ProjectGroup). Project связан с единственной, но обязательной Группой проекта.

Далее видим два перечисления: “Цель продвижения” (target_id, enum) и “Уровень экспертности в линкбилдинге” (expertLevel, enum).

Видим действие — создание. Записываем, что нужно добавить его operationId.

OpenAPI

База данных

Tags: Projects, ProjectGroups

Projects.addProject():

id
type: integer, minimum: 1, readOnly: true

domain
type: string, minLength: 3 (* нужен валидатор)

name
type: string, minLength: 1, maxLength: 100 (описываем по бизнес-требованиям из документации)

Таблица project

id
user_id -> User (понимаем из контекста, что проект принадлежит пользователю)
created_at (технические поля, их добавляем стандартно)
updated_at

Таблица project_group
id
project_id -> Project
name (* уточняем прочие поля по последующим макетам и документации)

Проектируем список проектов

Изучая очередной макет, мы насыщаем нашу базу знаний о предметной области, пополняя списки.

Выделяем блок над списком сущностей Project. Понимаем, что он обобщающий, поэтому связываем его не с Project, а непосредственно с пользователем. Заводим кроме сущности User связанную сущность UserCounters и заводим в ней счётчики. 

Добавляем признаки к Project: активность, архивированность, наличие ошибок. Из документации узнаём, что они вычисляемые — применяем денормализацию, помещаем в связанную таблицу.

Видим, что необходим метод быстрого подбора проекта. Можно реализовать эту функцию двумя способами: всегда иметь полный список проектов в кратком формате (Projects.getProjectsListBrief()) или выполнять подбор на бэкенде по мере ввода или после его окончания (Projects.searchProject()). Следует уточнить у бизнеса предпочтительный вариант реализации.

Итого

В этой статье мы познакомились со структурой спецификации и реальными примерами её использования. Также мы изучили методику проектирования и попрактиковались в составлении спецификации.

Благодаря внедрению описанного выше бизнес-процесса производства новых зон приложения и целых приложений мы смогли сократить время доставки новых функций. Figma позволяет быстро и корректно составлять макеты зон на основе Customer Journey Map, а OpenAPI, совместно с инструментарием — быстро их реализовывать. Наличие ясных соглашений в виде спецификации спасает от многих ошибок и здорово автоматизирует процесс разработки. OpenAPI стал настолько важной частью разработки, что уже невозможно представить, как работать без него.

Надеюсь, что наш опыт окажется полезен другим разработчикам.

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