В этой статье речь пойдёт о написании и поддержке полезной и актуальной спецификации для REST API-проекта, которая позволит сэкономить много лишнего кода, а также серьёзно улучшить целостность, надежность и прозрачность прокта в целом.


Что такое RESTful API?



Это миф.


Серьёзно, если вы думаете, что в вашем проекте RESTful API, вы почти наверняка ошибаетесь. Идея RESTful — в построении API, который во всём соответствовал бы архитектурным правилам и ограничениям, описанным стилем REST, однако в реальных условиях это оказывается почти невозможно.


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


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


Но подождите, не всё так плохо!


Зачем нужна спецификация REST API?


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


Чаще всего спецификация REST API ассоциируется с его документацией. В отличие от перво (которая является формальным описанием вашего API), документация предназначена для чтения людьми: например, разработчиками мобильного или веб-приложения, использующего ваш API.


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


  • сделать более простым и надёжным unit-тестирование;
  • настроить предобработку и валидацию входных данных;
  • автоматизировать сериализацию и обеспечить целостность ответов;
  • и даже воспользоваться преимуществами статической типизации.

OpenAPI



Общепринятым форматом для описания REST API на сегодняшний день является OpenAPI, который также известен как Swagger. Эта спецификация представляет из себя единый файл в формате JSON или YAML, состоящий из трёх разделов:


  • заголовок, содержащий название, описание и версию API, а также дополнительную информацию;
  • описание всех ресурсов, включая их идентификаторы, HTTP-методы, все входные параметры, а также коды и форматы тела ответов, со ссылками на определения;
  • все определения объектов в формате JSON Schema, которые могут использоваться как во входных параметрах, так и в ответах.

У OpenAPI есть серьёзный недостаток — сложность структуры и, зачастую, избыточность. Для небольшого проекта содержимое JSON-файла спецификации может быстро разрастись до нескольких тысяч строк. В таком виде поддерживать этот файл вручную невозможно. Это — серьёзная угроза для самой идеи поддержания актуальной спецификации по мере развития API.


Существует множество визуальных редакторов, позволяющих описывать API и формирующих в итоге спецификацию OpenAPI. На них в свою очередь основаны дополнительные сервисы и облачные решения, например сам Swagger, Apiary, Stoplight, Restlet и другие.


Однако, для меня подобные сервисы оказались не слишком удобными из-за сложности быстрого редактирования спецификации и совмещения с процессом написания кода. Ещё один минус — зависимость от набора функций каждого конкретного сервиса. Например, реализовать полноценное unit-тестирование только средствами облачного сервиса практически невозможно. Кодогенерация и даже создание "заглушек" для эндпоинтов, хоть и кажутся весьма возможными, на практике оказываются практически бесполезными.


Tinyspec


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


В статье я буду приводить примеры из Node.js (koa, express) и Ruby on Rails, хотя эти практики применимы к большинству технологий, включая Python, PHP и Java.


Когда ещё спецификация оказывается невероятно полезной


1. Unit-тесты эндпоинтов


Behavior-driven development (BDD) идеально подходит для разработки REST API. Удобнее всего писать unit-тесты не для отдельных классов, моделей и контроллеров, а для конкретных эндпоинтов. В каждом тесте вы эмулируете настоящий HTTP-запрос и проверяете ответ сервера. В Node.js для эмуляции тестовых запросов есть supertest и chai-http, в Ruby on Rails — airborne.


Предположим, у нас есть схема User и эндпоинт GET /users, возвращающий всех пользователей. Вот синтаксис tinyspec, который описывает это:


  1. Файл user.models.tinyspec:

User {name, isAdmin: b, age?: i}

  1. Файл users.endpoints.tinyspec:

GET /users
    => {users: User[]}

Вот так будет выглядеть наш тест:


Node.js


describe('/users', () => {
  it('List all users', async () => {
    const { status, body: { users } } = request.get('/users');

    expect(status).to.equal(200);
    expect(users[0].name).to.be('string');
    expect(users[0].isAdmin).to.be('boolean');
    expect(users[0].age).to.be.oneOf(['boolean', null]);
  });
});

Ruby on Rails


describe 'GET /users' do
  it 'List all users' do
    get '/users'

    expect_status(200)
    expect_json_types('users.*', {
      name: :string,
      isAdmin: :boolean,
      age: :integer_or_null,
    })
  end
end

Когда у нас есть спецификация, в которой описаны форматы ответа сервера, мы можем упростить тест и просто проверять ответ на соответствие этой спецификации. Для этого мы воспользуемся тем, что наши tinyspec-модели превращаются в OpenAPI-определения, которые в свою очередь соответствуют формату JSON Schema.


Любой literal object в JS (или Hash в Ruby, dict в Python, ассоциативный массив в PHP и даже Map в Java) можно протестировать на соответствие JSON-схеме. И даже есть соответствующие плагины для тестирующих фреймворков, например jest-ajv (npm), chai-ajv-json-schema (npm) и json_matchers (rubygem) для RSpec.


Перед тем как использовать схемы, надо подключить их в проект. Для начала сгенерируем на основе tinyspec файл спецификации openapi.json (это действие можно автоматически выполнять перед каждым запуском тестов):


tinyspec -j -o openapi.json

Node.js


Теперь мы можем использовать полученный JSON в проекте и взять из него ключ definitions, в котором находятся все JSON-схемы. Схемы могут содержать в себе перекрёстные ссылки ($ref), поэтому, если у нас есть вложенные схемы (например, Blog {posts: Post[]}), то нам необходимо "развернуть" их, чтобы использовать в валидациях. Для этого будем использовать json-schema-deref-sync (npm).


import deref from 'json-schema-deref-sync';
const spec = require('./openapi.json');
const schemas = deref(spec).definitions;

describe('/users', () => {
  it('List all users', async () => {
    const { status, body: { users } } = request.get('/users');

    expect(status).to.equal(200);
    // Chai
    expect(users[0]).to.be.validWithSchema(schemas.User);
    // Jest
    expect(users[0]).toMatchSchema(schemas.User);
  });
});

Ruby on Rails


json_matchers умеет обрабатывать $ref-ссылки, но требует наличия отдельных файлов со схемами в файловой системе по определённому пути, поэтому сначала придётся "разбить" swagger.json на множество мелких файлов (подробнее об этом тут):


# ./spec/support/json_schemas.rb
require 'json'
require 'json_matchers/rspec'

JsonMatchers.schema_root = 'spec/schemas'

# Fix for json_matchers single-file restriction
file = File.read 'spec/schemas/openapi.json'
swagger = JSON.parse(file, symbolize_names: true)
swagger[:definitions].keys.each do |key|
  File.open("spec/schemas/#{key}.json", 'w') do |f|
    f.write(JSON.pretty_generate({
      '$ref': "swagger.json#/definitions/#{key}"
    }))
  end
end

После этого наш тест мы сможем написать так:


describe 'GET /users' do
  it 'List all users' do
    get '/users'

    expect_status(200)
    expect(result[:users][0]).to match_json_schema('User')
  end
end

Обратите внимание: писать тесты подобным образом невероятно удобно. Особенно, если ваш IDE поддерживает запуск тестов и отладку (как, например, WebStorm, RubyMine и Visual Studio). Таким образом, вы можете вообще не использовать какое-либо другое ПО, а весь цикл разработки API сводится к 3 последовательным шагам:


  1. проектирование спецификации (например, в tinyspec);
  2. написание полного набора тестов на добавленные/изменённые эндпоинты;
  3. разработка кода, удовлетворяющего всем тестам.

2. Валидация входных данных


OpenAPI описывает формат не только ответов, но и входных данных. Это позволяет нам прямо во время запроса производить валидацию данных, пришедших от пользователя.


Предположим, у нас есть следующая спецификация, которая описывает обновление данных пользователя, а также все доступные для изменения поля:


# user.models.tinyspec
UserUpdate !{name?, age?: i}

# users.endpoints.tinyspec
PATCH /users/:id {user: UserUpdate}
    => {success: b}

Ранее мы рассматривали плагины для валидации внутри тестов, однако для более общих случаев существуют модули валидации ajv (npm) и json-schema (rubygem), давайте воспользуемся ими и напишем контроллер с валидацией.


Node.js (Koa)


Это пример для Koa — преемника Express, однако для Express код будет выглядеть похожим образом.


import Router from 'koa-router';
import Ajv from 'ajv';
import { schemas } from './schemas';

const router = new Router();

// Standard resource update action in Koa.
router.patch('/:id', async (ctx) => {
  const updateData = ctx.body.user;

  // Validation using JSON schema from API specification.
  await validate(schemas.UserUpdate, updateData);

  const user = await User.findById(ctx.params.id);
  await user.update(updateData);

  ctx.body = { success: true };
});

async function validate(schema, data) {
  const ajv = new Ajv();

  if (!ajv.validate(schema, data)) {
    const err = new Error();
    err.errors = ajv.errors;
    throw err;
  }
}

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


Добавим описание модели FieldsValidationError в файле error.models.tinyspec:


Error {error: b, message}

InvalidField {name, message}

FieldsValidationError < Error {fields: InvalidField[]}

А теперь укажем её как один из возможных ответов нашего эндпоинта:


PATCH /users/:id {user: UserUpdate}
    => 200 {success: b}
    => 422 FieldsValidationError

Такой подход позволит писать unit-тесты, проверяющие правильность формирования ошибки при некорректных данных, пришедших от клиента.


3. Сериализация моделей


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


Процесс формирования JSON-представления этих сущностей для передачи в ответе API называется сериализацией. Существует ряд плагинов для разных фреймворков, выполняющих функции сериализатора, например: sequelize-to-json (npm), acts_as_api (rubygem), jsonapi-rails (rubygem). По факту эти плагины позволяют для конкретной модели указать список полей, которые необходимо включить в JSON-объект, а также дополнительные правила, например для их переименования или динамического вычисления значений.


Сложности начинаются, когда нам необходимо иметь несколько отличающихся JSON-представлений одной модели или когда объект содержит вложенные сущности — ассоциации. Возникает необходимость в наследовании, переиспользовании и связывании сериализаторов.


Разные модули решают эти задачи по-разному, но давайте задумаемся, а может ли нам снова помочь спецификация? Ведь по сути вся информация о требованиях к JSON-представлениям, все возможные комбинации полей, включая вложенные сущности, уже находятся в ней. А значит мы можем написать автоматический сериализатор.


Предлагаю вашему вниманию небольшой модуль sequelize-serialize (npm), позволяющий сделать это для моделей Sequelize. Он принимает на вход экземпляр модели или массив, а также требуемую схему и итеративно строит сериализованный объект, учитывая все требуемые поля и используя вложенные схемы для ассоциированных сущностей.


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


# models.tinyspec
Comment {authorId: i, message}
Post {topic, message, comments?: Comment[]}
User {name, isAdmin: b, age?: i}
UserWithPosts < User {posts: Post[]}

# blogUsers.endpoints.tinyspec
GET /blog/users
    => {users: UserWithPosts[]}

Теперь мы можем построить запрос с помощью Sequelize и вернуть сериализованный объект, в точности соответствующий только что описанной выше спецификации:


import Router from 'koa-router';
import serialize from 'sequelize-serialize';
import { schemas } from './schemas';

const router = new Router();

router.get('/blog/users', async (ctx) => {
  const users = await User.findAll({
    include: [{
      association: User.posts,
      required: true,
      include: [Post.comments]
    }]
  });

  ctx.body = serialize(users, schemas.UserWithPosts);
});

Это почти магия, правда?


4. Статическая типизация


Если вы настолько круты, что используете TypeScript или Flow, возможно, вы уже задались вопросом "А как же мои дорогие статические типы?!". С помощью модулей sw2dts или swagger-to-flowtype можно сгенерировать все необходимые определения на основе JSON-схем и использовать для статической типизации тестов, входных данных и сериализаторов.


tinyspec -j

sw2dts ./swagger.json -o Api.d.ts --namespace Api

Теперь мы можем использовать типы в контроллерах:


router.patch('/users/:id', async (ctx) => {
  // Specify type for request data object
  const userData: Api.UserUpdate = ctx.request.body.user;

  // Run spec validation
  await validate(schemas.UserUpdate, userData);

  // Query the database
  const user = await User.findById(ctx.params.id);
  await user.update(userData);

  // Return serialized result
  const serialized: Api.User = serialize(user, schemas.User);
  ctx.body = { user: serialized };
});

И в тестах:


it('Update user', async () => {
  // Static check for test input data.
  const updateData: Api.UserUpdate = { name: MODIFIED };

  const res = await request.patch('/users/1', { user: updateData });

  // Type helper for request response:
  const user: Api.User = res.body.user;

  expect(user).to.be.validWithSchema(schemas.User);
  expect(user).to.containSubset(updateData);
});

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


5. Приведение типов query string


Если ваш API по какой-то причине принимает запросы с MIME-типом application/x-www-form-urlencoded, а не application/json, тело запроса будет выглядеть так:


param1=value&param2=777&param3=false

То же самое касается и query-параметров (например, в GET-запросах). В этом случае веб-сервер не сможет автоматически распознать типы — все данные будут в виде строк (вот обсуждение в репозитории npm-модуля qs), так что после парсинга вы получите такой объект:


{ param1: 'value', param2: '777', param3: 'false' }

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


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


# posts.endpoints.tinyspec
GET /posts?PostsQuery

# post.models.tinyspec
PostsQuery {
  search,
  limit: i,
  offset: i,
  filter: {
    isRead: b
  }
}

Вот пример запроса к такому эндпоинту


GET /posts?search=needle&offset=10&limit=1&filter[isRead]=true

Давайте напишем функцию castQuery, которая за нас приведёт все параметры к нужным типам. Она будет выглядеть примерно так:


function castQuery(query, schema) {
  _.mapValues(query, (value, key) => {
    const { type } = schema.properties[key] || {};

    if (!value || !type) {
      return value;
    }

    switch (type) {
      case 'integer':
        return parseInt(value, 10);
      case 'number':
        return parseFloat(value);
      case 'boolean':
        return value !== 'false';
      default:
        return value;
    }
 });
}

Её более полная реализация с поддержкой вложенных схем, массивов и null-типов доступна в cast-with-schema (npm). Теперь мы можем использовать её в нашем коде:


router.get('/posts', async (ctx) => {
  // Cast parameters to expected types
  const query = castQuery(ctx.query, schemas.PostsQuery);

  // Run spec validation
  await validate(schemas.PostsQuery, query);

  // Query the database
  const posts = await Post.search(query);

  // Return serialized result
  ctx.body = { posts: serialize(posts, schemas.Post) };
});

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


Лучшие практики


Отдельные схемы для создания и изменения


Обычно схемы, которые описывают ответ сервера, отличаются от тех, которые описывают входные данные, используемые при создании и изменении моделей. Например, список доступных полей при POST — и PATCH-запросах должен быть жестко ограничен, при этом в PATCH-запросах обычно все поля схемы делаются опциональными. Схемы же, определяющие ответ, могут быть более свободными.


В автоматической генерации CRUDL-эндпоинтов tinyspec используются постфиксы New и Update. Схемы User* могут быть определены следующим образом:


User {id, email, name, isAdmin: b}
UserNew !{email, name}
UserUpdate !{email?, name?}

Старайтесь не использовать одни и те же схемы для разных типов действий, чтобы избежать случайных проблем с безопасностью из-за повторного использования или наследования старых схем.


Семантика в названиях схем


Содержимое одних и тех же моделей может отличаться в разных эндпоинтах. Используйте постфиксы With* и For* в названиях схем, чтобы показать, чем они отличаются и для чего предназначены. В tinyspec модели также можно наследовать друг от друга. Например:


User {name, surname}
UserWithPhotos < User {photos: Photo[]}
UserForAdmin < User {id, email, lastLoginAt: d}

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


Разделение эндпоинтов по типу клиента


Часто одни и те же эндпоинты возвращают разные данные в зависимости от типа клиента или роли пользователя, обращающегося к эндпоинту. Например, эндпоинты GET /users и GET /messages могут сильно отличаться для пользователей вашего мобильного приложения и для менеджеров бэк-офиса. При этом изменение самого названия эндпоинта может быть слишком большим усложнением.


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


Mobile app:
    GET /users (mobile)
        => UserForMobile[]

CRM admin panel:
    GET /users (admin)
        => UserForAdmin[]

Документация REST API


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


Кроме упомянутых ранее облачных сервисов, существуют CLI-инструменты, конвертирующие OpenAPI 2.0 в HTML и PDF, после чего вы можете загрузить его на любой статический хостинг. Примеры:



Знаете ещё примеры? Поделитесь ими в комментариях.


К сожалению, вышедшая год назад OpenAPI 3.0 всё ещё мало поддерживается и мне не удалось найти достойных примеров документации на её основе: ни среди облачных решений, ни среди CLI-инструментов. По этой же причине OpenAPI 3.0 пока не поддерживается в tinyspec.


Публикация в GitHub


Один из самых простых способов публикации документации — GitHub Pages. Просто включите поддержку статических страниц для директории /docs в настройках вашего репозитория и храните HTML-документацию в этой папке.



Можно добавить команду для генерации документации через tinyspec или другой CLI-инструмент в scripts в package.json и обновлять документацию при каждом коммите:


"scripts": {
    "docs": "tinyspec -h -o docs/",
    "precommit": "npm run docs"
}

Continuous Integration


Вы можете включить генерацию документации в цикл CI и публиковать её, к примеру, в Amazon S3 под разными адресами в зависимости от окружения или версии вашего API, например: /docs/2.0, /docs/stable, /docs/staging.


Tinyspec Cloud


Если вам понравился синтаксис tinyspec, вы можете зарегистрироваться в качестве early adopter на tinyspec.cloud. Мы собираемся построить на его основе облачный сервис и CLI для автоматической публикации документации с широким выбором шаблонов и возможностью разрабатывать свои собственные шаблоны.


Заключение


Разработка REST API — пожалуй, самое приятное занятие из всех, что существуют в процессе работы над современными веб- и мобильными сервисами. Здесь нет зоопарка браузеров, операционных систем и размеров экранов, всё находится полностью под нашим контролем — "на кончиках пальцев".


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


Ведь по сути, если уж мы и занимаемся тем, что создаём миф, то почему бы нам не сделать его прекрасным?

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


  1. Angerslave
    25.10.2018 14:05

    [картинка про 14 конкурирующих стандартов]

    Любопытно, почему в качестве решения предлагается «лёгкий swagger», а не переход в надсистему, например, через GraphQL?


    1. runawayed Автор
      25.10.2018 14:13
      +1

      Смотря, о какой проблеме вы говорите. О том, что Swagger сложный или о том что «GraphQL лучше REST»? Во втором случае, боюсь, мы скатимся к очередной религиозной дискуссии, чего бы не хотелось.


      1. Angerslave
        25.10.2018 14:42

        Второе суждение сложно назвать истинным, ибо «лучше» в любом случае субъективно. Я скорее о том, что и OpenAPI и tinyspec являются тулзами для документации и многие аспекты, которые можно использовать для разработки эндпоинтов (валидация данные, написание тестов в вашем случае), являются следствием скорее строгого стандарта, чем артефактом самой технологии. В этом плане, по моему мнению, GraphQL предлагает эти аспекты как часть парадигмы, во многом даже объединяя в себе REST API, валидации, связывание данных и документацию всего этого дела. Опять же в силу строгости стандарта к нему также применимы различные свистелки и кряхтелки вроде быстрого написания тестов.

        Собственно в этом и был мой вопрос — зачем использовать «ещё один стандарт документации», если можно попробовать решить проблему шире? В общем-то, в такой постановке он довольно риторический…


  1. mikhailian
    25.10.2018 15:07

    Ссылка на оригинал не ведёт на оригинал, а открывает полноэкранную рекламу Toptal. Если нажать F5, то появится статья на английском.


  1. RouR
    25.10.2018 15:26

    У OpenAPI есть серьёзный недостаток — сложность структуры и, зачастую, избыточность. Для небольшого проекта содержимое JSON-файла спецификации может быстро разрастись до нескольких тысяч строк. В таком виде поддерживать этот файл вручную невозможно. Это — серьёзная угроза для самой идеи поддержания актуальной спецификации по мере развития API.

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


    1. runawayed Автор
      25.10.2018 17:08
      +2

      Генерироваться из чего? Если имеется ввиду сам код, то на практике это вряд ли реализуемо, учитывая возможную специфику реализации компонентов (роутов, экшнов и т.п.), а также всё множество различных технологий и фреймворков.


      Вообще первичной должна быть все же спецификация, которая затем «имплементируется» в виде кода.


      А какие проблемы вы видите с версионированием? В нескольких реальных проектах в течение продолжительного времени используем этот подход без каких-либо проблем.


      1. bromzh
        25.10.2018 17:22

        На моём прошлом месте работы описание АПИ в формате сваггера успешно генерируется при деплое и развёртывании приложения (Java + Spring). Доп. инфа описывается через аннотации.
        Да, такое не для каждого языка/фреймворка есть, но для большинства популярных вполне.


        1. runawayed Автор
          25.10.2018 17:59
          +1

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


        1. abyrvalg
          26.10.2018 05:31

          Это замечательно работает, если вы просто предоставляете публичное АПИ.

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

          В таких случаях гораздо лучше работает подход documentation first. Все заинтересованные стороны сообща разрабатывают спецификацию API в понятном всем формате (swagger, openAPI), утверждают её и только потом приступают к разработке.


      1. RouR
        25.10.2018 17:23

        Генерироваться из чего? Если имеется ввиду сам код, то на практике это вряд ли реализуемо

        А это зависит от выбранных инструментов/языка программирования.
        Я пишу под .Net, для автогенерации есть либа, подключить — несколько строчек.
        Для Java и Php наверняка тоже есть аналоги, reflection api ведь у них есть.
        Для Js, ну да, тяжелый случай, есть нет Typescript.

        А какие проблемы вы видите с версионированием?

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


        1. runawayed Автор
          25.10.2018 18:08

          Большого количества версий быть не должно. В общем случае оно должно соответствовать количеству доступных для пользателей версий API, и обычно их не так много: например, stable и prestable. Хранятся эти версии обычно в разных ветках, так что сложностей не возникает.


          1. LighteR
            26.10.2018 01:07

            Хранятся эти версии обычно в разных ветках

            Это как? У вас в проде постоянно несколько бранчей задеплоено? Серьезно?
            и обычно их не так много: например, stable и prestable.

            Странное название версий. И что вы делаете с клиентами, которые используют stable и вы хотите ваш prestable сделать stable?


            1. runawayed Автор
              26.10.2018 12:32

              Это был абстрактный пример. Всё сильно зависит от конкретной задачи и многих факторов, например, публичный ли это API или внутренний. Разные версии могут быть определены ветками или тегами, это не принципиально. Prestable это чаще всего не «прод», а staging. В каких-то случаях это может быть alpha или beta. Стратегии релизов и поддержки обратной совместимости также специфичны для разных проектов.

              Именно поэтому как раз очень важно нахождение спецификации рядом с кодом и совместное версионирование, вкупе с CI.


      1. jakobz
        26.10.2018 11:08
        +1

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


    1. Nikita_Danilov
      25.10.2018 23:22

      Вот абсолютно согласен.
      Претензия не к самой статье, а в целом было огромным удивлением узнать:
      Заканчивается 2018 год, а фанаты "прогрессивных" языков до сих пор учатся генерировать WSDL/OpenAPI/ из метаданных сборки/кода.


  1. CheY
    25.10.2018 16:52
    +1

    Поделюсь опытом использование таких тулз, а точнее Blueprint API.

    Исходное состояние:
    — продукты средней сложности
    — API прежде всего для поддержки работы SPA и мобильных приложений, но также есть и межсервисное взаимодействие
    — подход к разработке во многом выработался в духе «API-first» — API целенаправленно пишется постановщиками чуть ли не в первую очередь, согласовывается потребителями и нужен чуть ли не всем участникам: менеджерам, аналитикам, разработчикам, тестировщикам и иногда даже заказчикам или сторонним подрядчикам.

    Долгое время всё это велось в confluence с не строгим шаблоном. Это вызывало следующее проблемы:
    1. Отсутствие регламента и прозрачности в изменениях. Хотя круг тех, кто имел право внесения правок был ограничен, но всё же их было сложно контроллировать. Из-за этого возникали проблемы в духе «зачем вы это поменяли?», «а я не знал?», «в рамках чего было это изменение?» и прочее.
    2. Отсутствие формализации описания там, где она была бы полезна. Достаточные ограничения на типы полей, примеры, единообразность, наглядность — всего этого часто не хватало. Легко можно было обнаружить, например, поле даты где-то в виде UNIX timestamp, а где-то в виде ISO8601 в одном и том же контексте.
    3. Сложность использования для валидации или тестирования. Во многом вытекает из первых двух пунктов и обычных людских ошибок. Даже если каждый метод снабжался JSON-схемой и примерами запросов и ответов, то это не гарантировало того, что они актуальные и вообще правильные.

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

    Как это выглядело.
    По сути был почти полностью перенят процесс работы разработчиков и применён к работе аналитика над API. Основные моменты:
    — Любое изменение только по задаче в Jira. К этому не надо относиться как к лишней бюрократии — это просто момент инициации работы. Даже какое-то подробное объяснение не требуется.
    — Всё API — в git. Любое изменение проводится близко к git flow — именование веток, пулреквесты, мерджи в нужные ветки.
    — В пулреквест в качестве ревьюверов доабвляются все заинтересованные лица со стороны разработки. После обсуждения, правок и апрувов ветка может быть смержена, а изменение в апи опубликовано.
    — Весь тулчейн настроен в CI и работа аналитика сведена только к текстовому редактору и git'у.

    Какие плюсы поимели:
    — Процесс работы над API стал формализованным и прозрачным для всех.
    — Blueprint предоставил достаточные средства для формализации описания. Да, он не идеальный и многих вещей в нём не хватает, а что-то было сделано криво. Но уже через несколько итераций выстроилась достаточно удобная схема ведения спецификации, организации файлов, соглашений по именованию сущностей внутри Blueprint API. Какие-то фичи были добавлены нами, какие-то баги поправлены.
    — Минимизация ошибок из-за человеческого фактора. За счёт переиспользования сущностей внутри спецификации и автоматической генерации примеров, JSON-схемы и табличного описания полей, аналитик получал по сути конструктор методов, на выходе которого получалось удобное для всех заинтересованных лиц представление.
    — Автоматизация и валидация. Тут очевидные применения всего этого добра — автогенерация интерфейсов к API (прежде всего у мобильщиков), валидация ответов при тестировании и прочее.

    Какие проблемы возникли:
    — Git flow это не только благо. Всё же документация это не код. В документации много изменяемых, но не формализуемых частей — текстовые описания методов, тексты ошибок, таблицы прав и прочее.
    — Сложности синхронизации с командой и процессами разработки. Зачастую документация писалась сильно вперёд. Аналитик мог описывать функциональность на 1 и 2 релиза вперёд, а значит накидывать в git ветки, которые могли не скоро понадобится. А если в это вмешивались изъяны процесса разработки, то даже порядок будущего мерджа ветвей нельзя было предсказать. Вкупе с первым пунктом это вытекало либо в трудности при мердже (конфликты, cherry-picking, вот это всё), либо в сложности с определением какая часть документации актуальна, а какая уже ускакала вперёд.
    — Повышенный overhead на работу аналитика. Раньше он просто редактировал страничку в confluence, а теперь он ломает голову над декомпозицией структур в Blueprint API, над ветками, пулреквестами и прочим. Можно сказать, что это повышает нижнюю планку для аналитика в проекте.
    — Организационные моменты. Просмотр PR'ов по документации было правом, но не обязанностью заинтересованных лиц. Они тратили на это время, но это время не было их должностной обязанностью. В итоге со временем многие начали забивать на ревью.
    — Ограничения Blueprint API. Да, многого не хватает. Да, редко, но были ситуации, когда API подстраивалось под возможности документирования хотелки. Да, это куча разрозненных файлов (в лучшем случае — насколько я знаю, в Swagger'е это просто диких размеров yaml), скреплённых придуманными вами соглашениями и правилами.

    По итогам почти года использования этой штуки (ах да, я как раз в роли аналитика выступал) на проекте «с нуля» и до «n+1-ого релиза», могу сказать, что это был положительный опыт. Да, многое было сделано неудобно, но процесс создания и ведения API встал на некие рельсы, стал более выделенным. И все потребители оказались удовлетворены — менеджеры получили человечное и красивое опсиание, тестировщики получили почти исчерпывающие данные об ограничениях API, разработчики получили предсказуемость, полноту и возможность накручивания новых тулз поверх формального описания Blueprint API.


    1. polar11beer
      26.10.2018 11:03

      Да, это куча разрозненных файлов (в лучшем случае — насколько я знаю, в Swagger'е это просто диких размеров yaml)

      Можно разбить спеку на части и ссылаться друг на друга через $ref-ы. Или это не помогает?


      1. yarkov
        26.10.2018 11:18

        Поддержу. Сам пользуюсь пакетом multi-file-swagger. Там в самом начале ссылка на статью с примером использования.


        1. polar11beer
          26.10.2018 12:01

          Про плагин не знал. Спасибо, надо посмотреть.


      1. CheY
        26.10.2018 11:56

        Я сваггер только мельком смотрел. Может какие-то расширения к нему позволяют разбивать на части спеку.
        В Blueprint API для этого есть что-то вроде «наследования» и «переопределения полей» и возможность инклудить одни документы в другие (но эта фича не из коробки). Если добавить соглашений наименования и разбиения на файлы, то в целом становится удобоваримо.


        1. polar11beer
          26.10.2018 12:01

          В Swagger-е это можно делать просто руками: github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#document-structure
          Но да, соглашения наименования и разбиения всё равно нужны.


  1. Fedcomp
    25.10.2018 16:55
    -2

    Все что угодно вместо того чтобы генерировать документацию в тестах.


  1. LighteR
    26.10.2018 00:56

    Удобнее всего писать unit-тесты не для отдельных классов, моделей и контроллеров, а для конкретных эндпоинтов.

    Вы путаете unit-тесты и интеграционные тесты. Они должны не заменять друг друга, а дополнять.
    У OpenAPI есть серьёзный недостаток — сложность структуры

    Как уже заметили выше, обычно swagger-схема генерируется автоматически. Т.ч. к недостаткам это можно отнести с натяжкой


  1. iproger
    26.10.2018 06:36

    За 3 года работы со swagger мне он все еще нравится. Конечно решает генерация библиотеки из схемы. В основном пользовался готовой схемой к проекту.


  1. antirek
    26.10.2018 07:45

    Для node.js есть хороший npm для описания и генерации openapi spec прямо в коде в связке с express.js express-openapi, и еще несколько сопутствующих пакетов github.com/kogosoftwarellc/open-api


  1. RomanGotsiy
    26.10.2018 09:36
    +1

    У OpenAPI есть серьёзный недостаток — сложность структуры и, зачастую, избыточность. Для небольшого проекта содержимое JSON-файла спецификации может быстро разрастись до нескольких тысяч строк. В таком виде поддерживать этот файл вручную невозможно. Это — серьёзная угроза для самой идеи поддержания актуальной спецификации по мере развития API.


    Не обязательно держать все в одном большом JSON-файле, можно же разбить как например здесь: github.com/Rebilly/RebillyAPI/tree/master/spec
    + использование YAML вместо JSON улучшает «сапортабилити» в разы.


    1. runawayed Автор
      26.10.2018 12:38

      Хороший пример!


  1. polar11beer
    26.10.2018 10:56

    К сожалению, вышедшая год назад OpenAPI 3.0 всё ещё мало поддерживается и мне не удалось найти достойных примеров документации на её основе

    Поначалу да, но сейчас в Swagger UI уже таки поддерживается большинство возможностей OpenAPI 3.0.


    1. runawayed Автор
      26.10.2018 12:22

      Да, они так заявляют, но найти хороший пример мне не удалось. Поделитесь, если знаете.


      1. polar11beer
        26.10.2018 14:36

        Стандартный petstore вполне неплох. Из основного функционала OpenAPI 3.0 в Swagger UI не поддерживаются домены, это да. С другими ограничениями пока не сталкивался.