Noveo

Наш блог Думы о web API-3

Думы о web API-3

Приближение третье: Запросы и ответы

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

Универсальный ответ

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

Success — маркер успешности выполнения запроса

Для того, чтобы при получении ответа от сервера сразу понять, увенчался ли запрос успехом, и передать его соответствующему обработчику, достаточно использовать маркер успешности ‘success’. Самый простой ответ сервера, не содержащий никаких данных, будет выглядеть так:

POST /api/v1/articles/22/publish
{
   “success”: true
}

Error — сведения об ошибке

В случае, если выполнение запроса завершилось неудачей — о причинах и разновидностях отрицательных ответов сервера поговорим чуть позже, — к ответу добавляется атрибут ‘error’, содержащий в себе HTTP-код статуса и текст сообщения об ошибке. Прошу не путать с сообщениями об ошибках валидации данных для конкретных полей. Правильнее всего, на мой взгляд, возвращать код статуса и в заголовке ответа, но я встречал и другой подход — в заголовке всегда возвращать статус 200 (успех), а детали и возможные данные об ошибках передавать в теле ответа.

GET /api/v1/user
{
    “success”: false,
    “error”: {
        “code” : 401,
        “message” : ”Authorization failed”
    }
}

Data — данные, возвращаемые сервером

Большинство ответов сервера призваны возвращать данные. В зависимости от типа запроса и его успеха ожидаемый набор данных будет разным, тем не менее атрибут ‘data’ будет присутствовать в подавляющем большинстве ответов.

Пример возвращаемых данных в случае успеха. В данном случае ответ содержит запрашиваемый объект user.

GET /api/v1/user
{
    “success”: true,
    “data”: {
        “id” : 125,
        “email” : ”john.smith@somedomain.com”,
        “name” : ”John”,
        “surname” : ”Smith”,
    }
}

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

PUT /api/v1/user
{
    “success”: false,
    “error”: {
        “code” : 422,
        “message” : ”Validation failed”
    }
    “data”: {
        “email” : ”Email could not be blank.”,
    }
}

Pagination — сведения, необходимые для организации постраничной навигации

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

Минимальный набор значений для пагинации состоит из:

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

Некоторые разработчики web API также включают в пагинацию набор готовых ссылок на соседние страницы, а также первую, последнюю и текущую.

GET /api/v1/articles
Response:
{
    “success”: true,
    “data”: [
        {
            “id” : 1,
            “title” : ”Interesting thing”,
        },
        {
            “id” : 2,
            “title” : ”Boring text”,
        }
    ],
    “pagination”: {
        “totalRecords” : 2,
        “totalPages” : 1,
        “currentPage” : 1,
        “perPage” : 20,
        “maxPerPage” : 100,
    }
}

Работа над ошибками

Как уже упоминалось выше, не все запросы к web API завершаются успехом, но это тоже часть игры. Система информирования об ошибках является мощным инструментом, облегчающим работу клиента и направляющим клиентское приложение по правильному пути. Слово “ошибка” в этом контексте не совсем уместно. Здесь больше подойдёт слово исключение, так как на самом деле запрос успешно получен, проанализирован, и на него возвращается адекватный ответ, объясняющий, почему запрос не может быть выполнен.

Каковы же потенциальные причины получаемых исключений?

500 Internal server error — всё сломалось, но мы скоро починим

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

400 Bad request — а теперь у вас всё сломалось

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

401 Unauthorized — незнакомец, назови себя

Для доступа к этому ресурсу требуется авторизация. Разумеется, наличие авторизации не гарантирует того, что ресурс станет доступным, но не авторизовавшись, вы точно этого не узнаете. Возникает, например, при попытке обратиться к закрытой части API или при истечении срока действия текущего токена.

403 Forbidden — вам сюда нельзя

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

404 Not found — по этому адресу никто не живёт

Такой ответ возвращается, как правило, в трёх случаях: путь к ресурсу неверен (ошибочен), запрашиваемый ресурс был удалён и перестал существовать, права текущего пользователя не позволяют ему знать о существовании запрашиваемого ресурса. Например, пока просматривали список товаров, один из них внезапно вышел из моды и был удалён.

405 Method not allowed — нельзя такое делать

Эта разновидность исключения напрямую связана с использованным при запросе глаголом (GET, PUT, POST, DELETE), который, в свою очередь, свидетельствует о действии, которое мы пытаемся совершить с ресурсом. Если запрошенный ресурс не поддерживает указанное действие, сервер говорит об этом прямо.

422 Unprocessable entity — исправьте и пришлите снова

Одно из самых полезных исключений. Возвращается каждый раз, когда в данных запроса существуют логические ошибки. Под данными запроса мы подразумеваем либо набор параметров и соответствующих им значений, переданных методом GET, либо поля объекта, передаваемого в теле запроса методами POST, PUT и DELETE. Если данные не прошли валидацию, сервер в секции ‘data’ возвращает отчет о том, какие именно параметры невалидны и почему.

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

Запросы

Для себя я классифицировал запросы к API следующим образом:

1. Запросы к коллекциям

  • Получение элементов коллекции (GET)
  • Добавление элемента в коллекцию (POST)
  • Обращение к свойству коллекции (GET)
  • Вызов функции у коллекции (POST)

2. Запросы к объектам

  • Получение объекта (GET)
  • Получение свойства объекта (GET)
  • Редактирование объекта (PUT/PATCH)
  • Редактирование свойства объекта (PUT/PATCH)
  • Удаление объекта (DELETE)
  • Установка и разрыв связей (как частный случай функции) (POST)
  • Вызов прочих функции у объекта (POST)

3. Работа с файлами

  • Загрузка файла на сервер (POST)
  • Получение файла (GET)

Получение элементов коллекции

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

Постраничная навигация

page — параметр указывает на то, какая страница должна быть отображена. Если этот параметр не передан, то отображается первая страница. Из первого же успешного ответа сервера будет ясно, сколько страниц имеет коллекция при текущих параметрах фильтрации. Если значение превышает максимальное число страниц, то разумнее всего вернуть ошибку 404 Not found.

GET /api/v1/news?page=1

perPage — указывает на желаемое число элементов на странице. Как правило, API имеет собственное значение по умолчанию, которое возвращает в качестве поля perPage в секции pagination, но в ряде случаев позволяет увеличивать это значение до разумных пределов, предоставив максимальное значение maxPerPage:

GET /api/v1/news?perPage=100

Сортировка результатов

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

sortBy — существует несколько подходов к передаче данных о сложной сортировке в GET параметрах. Здесь необходимо четко указать порядок сортировки и направление.

В некоторых API это предлагается сделать в виде строки:

GET /api/v1/products?sortBy=name.desc,price.asc

В других вариантах предлагается использовать массив:

GET /api/v1/products?
sortBy[0][field]=name&
sortBy[0][direction]=desc&
sortBy[1][field]=price&
sortBy[1][direction]=asc

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

Простая фильтрация по значению

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

GET /api/v1/articles?authorId=25

Усложнённые варианты фильтрации

Многие интерфейсы требуют более сложной системы фильтрации и поиска. Перечислю основные и наиболее часто встречаемые варианты фильтрации.

Фильтрация по верхней и нижней границе с использованием операторов сравнения from (больше или равно), higher (больше), to (меньше или равно), lower (меньше). Применяется к полям, значения которых поддаются ранжированию.

GET /api/v1/products?price[from]=500&price[to]=1000

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

GET /api/v1/products?status[]=1&status[]=2

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

GET /api/v1/users?name[like]=John
GET /api/v1/products?code[like]=123

Именованные фильтры

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

GET /api/v1/products?filters[]=recommended

Именованные фильтры могут также иметь свои параметры.

GET /api/v1/products?filters[recommended]=kidds

Получение объектов

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

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

GET /api/v1/articles/25

А для доступа ко вторым мы используем уникальный путь к ресурсу:

GET /api/v1/myProfile

В обоих случаях мы получим в ответ набор свойств запрашиваемого объекта и их значения, являющиеся, как правило, скалярными величинами (строками, числами, булевым типом).

GET /api/v1/articles/25
Response:
{
    “success”: true,
    “data”:  {
        “id” : 25,
        “createdAt” : “2017-01-01 12:45”,
        “title” : ”Cool article”,
        “text” : ”This is a very interesting thing...”
    }
}

Управление набором возвращаемых полей

Я часто встречал примеры web API, в которых в запросе на получение отдельного объекта в качестве параметров перечислялись необходимые клиенту поля объекта. В связи с этим хочется напомнить, что мы говорим не об абстрактном плоском хранилище данных, коим зачастую является классическое RESTFul API в вакууме, а о более сложном, спроектированном под конкретные цели и задачи web API, где главенствующую роль играет серверная сторона. Иными словами, серверная сторона лучше знает, какой набор данных возвращать клиенту, и имеет довольно чёткое представление о том, для каких целей они будут использованы. На практике довольно часто разработка клиентского и серверного приложения идут рука об руку, постоянно координируя взаимодействие между собой, поэтому управление набором полей допустимо только как редкое исключение, применяемое для решения уж очень нетривиальных задач.

Связанные сущности, списки и агрегированные поля

Довольно часто клиентскому приложению для корректной работы нужен не только сам объект, но и ряд связанных с ним объектов, например, удобнее получать статью вместе с автором и последними комментариями, а комментарии, в свою очередь, вместе с авторами. Каждый связанный объект или список возвращается в виде так называемого псевдополя объекта, которое не является его собственным свойством, но указывает на связь. В итоге, запросив один объект, мы получаем, по сути, солидную иерархическую структуру, которая состоит как из единичных объектов вроде пользователя, так и из списков объектов. Разумеется, требуемая вложенность обсуждается “на берегу” в процессе проектирования взаимодействия клиентского приложения и серверной стороны. Помимо связанных сущностей и списков, к собственным свойствам объекта прибавляются также агрегированные на лету значения, указывающие, например, на общее число связей и служащие для подгрузки данных по требованию.

В случае, описанном выше, мы получим примерно следующий результат:

GET /api/v1/articles/25
Response:
{
    “success”: true,
    “data”:  {
        “id” : 25,
        “title” : ”Cool article”,
        “text” : ”This is a very interesting thing...”,
        “userId”: 10,
        “user” : {
            “id” : 10,
            “name” : “John Smith”,
        },
        “totalComments” : 17,
        “recentComments” : [
            {
        “id” : 40,
                “text” : ”It is great!”,
                “userId”: 20,
                “user”: {
                    “id” : 21,
                    “name” : “Michael Black”,
                 },
            }
        ]
    }
}

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

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

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.

НазадПредыдущий пост ВпередСледующий пост

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: