Лучшие практики проектирования REST API. REST API - что это? REST: перевод

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

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

Понять на самом базовом уровне, что такое REST и зачем он нужен, можно из статьи Райана Томайко “Как я объяснил жене, что такое REST”. Тем не менее, мы скорее подойдем к вопросу с технической стороны. Итак, приступим.

Что такое REST?

Representational state transfer (передача состояния управления) — это архитектурный стиль, применяемый при разработке веб-сервисов, и устанавливающий 6 правил для их построения.

Соблюдающие эти правила веб-сервисы называют RESTful сервисами. Помимо этого, REST также требует использование большинства возможностей протокола http.

Итак, что же это за правила?

  • Uniform Interface — единый интерфейс
  • Stateless — отсутствие состояний
  • Cacheable — кэширование
  • Client-Server — разграниченная архитектура клиент-сервер
  • Layered System — многоуровневая система
  • Code on Demand — код по запросу

Поговорим о каждом из них более подробно.

Единый интерфейс

REST архитектура подразумевает определение единого интерфейса взаимодействия всех клиентов с сервером.

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

Сам интерфейс должен разделять работу с разными ресурсами приложения по разным URL-адресам. Благодаря правильно выполненному разделению запросов, появляется возможность для лёгкого разделения одного веб-приложения на несколько, а это, в свою очередь, значительно облегчает портируемость и масштабируемость приложения.

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

Также REST требует, чтобы названия ресурсов представляли собой существительные во множественном числе. Не следует перегружать систему множеством URL. Каждый ресурс в самом простом случае должен иметь только 2 URL адреса. Например:

  • Формат для получения коллекции элементов: /buildings
  • Формат для получения элемента по id: /buildings/{id}

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

Тем не менее, некоторые веб-сервисы умеют поддерживать работу с несколькими форматами одновременно.

В таком случае формат возвращаемых данных сервером и формат данных, который сервер должен обработать, управляется с помощью HTTP заголовков Accept и Content-type. В заголовках могут передаваться разные типы. Например: application/json или application/xml.

Отсутствие состояний

Каждый запрос в RESTful-сервисе должен уникально идентифицировать себя ресурсом или ресурсами, и оперировать их полными состояниями. REST веб-сервисы не должны хранить какие-либо данные в сессиях запросов или cookies.

Кэширование

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

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

Клиент-сервер

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

  • клиенты не должны иметь представления о хранимой на сервере информации, так как это зона ответственности сервера.
  • Сервер не должен быть разработан в привязке к UI какого-либо клиента.

Layered system

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

Code on demand

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

Почему именно REST?

Несмотря на то, REST подходит для всех проектов, работающих по http, иногда его реализация может быть невозможна в силу недостаточных навыков команды. Тем не менее, если освоить REST должным образом, перед разработчиками открывается множество преимуществ:

  • Улучшенная производительность. REST возвращает исключительно данные, указанные в запросе. Таким образом быстрее обрабатываются запросы и выдаются ответы.
  • Масштабируемость. Приложение может работать на множестве серверов, что позволяет балансировать нагрузку между ними. Также сервис можно разделить на несколько “микросервисов”, которые смогут работать параллельно друг другу.
  • Портируемость благодаря единому интерфейсу
  • Прозрачность взаимодействия — за счёт своей стандартизации API остаётся понятным для пользователя.
  • Легкость изменений. Благодаря меньшей связанности кода, снижается вероятность “поломать” запросы при внесении изменений в другие части приложения.

Методы http

Http содержит 4 метода: GET, POST, PUT, DELETE. Каждый метод должен использоваться для разных целей и идентифицировать функционал, который запрос реализует.

  • GET. Используется для получения ресурсов (список ресурсов или один ресурс с указанием его id)
  • POST. используется для создания ресурсов
  • PUT. Ииспользуется для обновления ресурсов по id, который передается в URL
  • DELETE. Используется для удаления ресурсов по id, который передается в URL

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

Http статус-коды

При использовании REST для веб-сервисов необходимо правильно подобрать http статусы для соответсвуюших ответов сервера. Сам по себе http имеет несколько десятков статус-кодов, но мы приведём 10 наиболее часто используемых:

  1. 200 OK — хорошо
  2. 201 Created — создано
  3. 204 No Content — нет содержимого
  4. 304 Not modified — не изменялось
  5. 400 Bad request — неверный запрос
  6. 401 Unauthorized — не авторизован
  7. 403 Forbidden — запрещён
  8. 404 Not found — не найдено
  9. 409 Conflict — конфликт
  10. 500 Internal Server error — внутренняя ошибка сервера

Http статус-коды имеют большое количество кодов для обработок ошибок, но этих кодов не достаточно для любого приложения.

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

Дополнительно

  • Запросы, возвращающие коллекции элементов, должны иметь возможность пагинации, сортировки, фильтрации
  • Даты и время в запросах следует передавать в формате unix timestamp в миллисекундах.
  • Версия приложения должна быть зашита в URL приложения как node. Например: api.app.com/v1/buildings

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

Зачем, например, заморачиваться с методом DELETE или там заголовком Accept? Не проще ли использовать метод GET и передавать все в параметрах, например, delete=true или format=json ? Вбил в браузере, и работает! А вот этот ваш DELETE так просто через браузер не пошлешь. На что я ответил примерно так.

Вот, допустим, у вас есть некоторые ресурсы. Для определенности, пусть это будут книги и пользователи. Что, собственно, означает иметь REST API для работы с этими ресурсами? В первом приближении, следующее. Если мы хотим получить какую-то книгу, то говорим GET /books/123 . Аналогично информация о пользователе получается запросом GET /users/456 . Вообще-то, в начале URL неплохо бы иметь что-то вроде /api/v1.0/ , но для краткости мы это опустим. По умолчанию данные отдаются, например, в JSON’е , но при желании мы можем передать Accept-заголовок с другим форматом. Для создания или обновления существующей книги следует использовать метод PUT, передав данные в теле запроса и указав формат этих данных в заголовке Content-type. Для удаления данных используется метод DELETE.

Внимательный читатель спросит, а для чего тогда нужен POST? Вообще, если делать все по науке, он должен использоваться для добавления элементов в сущность, словно она является неким контейнером, например, словарем. Однако на практике так обычно не делают, ведь при использовании API несколькими клиентами один клиент может изменить название книги, а второй — ее цену, в результате чего получится ерунда. Поэтому POST либо вообще не используют, либо используют в качестве замены методов PUT и DELETE. То есть, POST с каким-то телом запроса работает, как PUT, а без тела запроса — как DELETE. Это позволяет работать с клиентами, которые почему-то не умеют посылать PUT и DELETE.

Можно работать и сразу с целыми коллекциями. Для получения списка всех пользователей говорим GET /users , а для создания нового пользователя с автоматически сгенерированным id — POST /users . Как и ранее, в последнем случае данные передаются в теле запроса. Также можно перезаписать всю коллекцию, сказав PUT /users , и удалить сразу всех пользователей, сказав DELETE /users . Еще иногда требуется фильтрация по полям или пагинация, в этих случаях делают так:

GET /api/v1.0/users?fields=id,email,url&offset=100&limit=10&order_by=id

… или как-то так:

GET /api/v1.0/logs?from=2013-01-01+00:00:00&to=2013-12-31+23:59:59

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

В свое время я имел удовольствие работать над проектом, где API был устроен «простым и понятным» образом, на методах GET и POST, со всякими delete=1 и так далее. Смею вас заверить, что на самом деле вы этого не хотите. Потому что на практике работа с этим API превращается в какой-то кошмар.

Допустим, один программист занимается книгами, а второй пользователями. Первый решает, что для получения списка всех сущностей будет использоваться запрос GET /all_books , а второй решает перечислять только id и использовать URL GET /select_user_ids . Для удаления сущности первый программист решает использовать параметр del=true , а второй — delete=1 . Для экспорта данных в CSV первый программист делает поддержку export=text/csv , а второй — format=CSV . Потом выясняется, что некоторые библиотеки не умеют посылать GET-запросы со слишком длинными query string и ходить за данными на чтение начинают методом POST. А затем кто-то случайно удаляет через браузер всех пользователей в боевом окружении… И так далее, и тому подобное, полный бардак в общем.

Вы спросите, что же мешает привести все это безобразие в одному стандарту, например, использовать только del=1 и export=csv ? Так вот, REST — это и есть то самое приведение к одному стандарту , с учетом всяческих граблей типа случайного удаления данных через браузер и так далее. Притом у разных компаний этот стандарт одинаковый. Когда в команду разработчиков приходит новичок, вы просто говорите ему, что у вас всюду REST, а основные ресурсы — это пользователи и книги. Все, после этого одного предложения ваш новый коллега знает 90% API, безо всякого там чтения Wiki. Если вы хотите говорить с иностранцами, вы же просто используете общепринятый английский язык , а не изобретаете новый? Вот так же и здесь. Нельзя также не напомнить о пользе повторного использования протоколов и кода. А ведь для работы с REST, и HTTP вообще, написана куча библиотек и фреймворков.

Вы скажите «я, конечно, согласен, что REST такой весь из себя интуитивно понятный и общепринятый, но что, если я просто хочу загрузить через браузер список книг в формате CSV»? Тут важно понимать, что REST — это не о том, как сделать все через браузер . Предполагается, что должен быть клиент, который умеет работать с вашим API, вот через него и экспортируете. Но если по каким-то причинам это затруднительно, вы можете, например, использовать curl. Если у вас нелады с консолью, вы без труда найдете множество GUI-клиентов или, скажем, какой-нибудь плагин для Chrome, с аналогичным функционалом. Однако я все же советую попробовать curl. Пользоваться им совсем не так сложно, как вам может казаться. Всего-то нужно запомнить десяток параметров.

Так задаются дополнительные HTTP-заголовки:

H "Accept: text/csv" -H "Content-type: application/json"

Выбираем используемый метод:

X{GET|PUT|POST|DELETE}

Указываем тело запроса:

D "{"name":"Alex","url":"http://сайт/"}"

D @filename.json
# чтобы при этом не удалялись символы новой строки:
--data-binary @filename.json

Выводим заголовки из ответа сервера в stdout:

Говорим передавать данные в gzip’е:

Сохраняем тело ответа в указанный файл вместо stdout:

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

Теперь рассмотрим пару примеров.

Экспорт книг в формате CSV:

curl -H "Accept: text/csv" http://localhost/api/v1.0/books -o books.csv

Создание пользователя c выводом заголовков из ответа сервера в stdout:

curl -XPOST -H "Content-type: application/json" -d "{"name":"Alex"}" \
http://localhost/api/v1.0/users -D -

Удаление пользователя с заданным id:

curl -XDELETE http://localhost/api/v1.0/users/123

Несложно, правда ведь?

Несколько финальных замечаний, относящихся не совсем к REST. Во-первых, иногда от приложения требуется не только предоставлять доступ к некоторым ресурсам, но и выполнять какие-то команды. Таким командам имеет смысл выделять URL-адреса, начинающиеся с /commands/ . Например, запуск почтовой рассылки по всем пользователям будет выглядеть как-то так:

curl -XPOST -H "Content-type: application/json" \
-d "{"subject":"Good news, everyone!","body":"..."}" \
http://localhost/api/v1.0/commands/notify_all_users_via_email

Дополнение: Некоторые команды должны быть доступны только в тестовом окружении, для них можно выделить URL-адреса, начинающиеся с /debug/ .

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

curl -H "Accept: application/x-json-stream" \
http://localhost/api/v1.0/streams/users -N

{"type":"user","data":{"id":123,"name":"Alex","url":"http://сайт/"}}
{"type":"user","data":{"id":456,"name":"Bob","url":"http://ya.ru/"}}
...
{"type":"sync"}
{"type":"heartbeat"}
{"type":"heartbeat"}
{"type":"user_deleted","data":{"id":123}}
...

Нужно обратить внимание на несколько моментов. Здесь используется формат x-json-stream , то есть, поток JSON-объектов, разделенных символом \n. Если этот символ встречается в самом JSON-объекте, его, соответственно, следует кодировать. Некоторым клиентам может быть удобнее работать с честным JSON’ом, то есть, списком JSON-объектов. Предусмотреть поддержку сразу нескольких форматов довольно просто. Во втором случае список объектов должен начинаться с открывающейся квадратной скобки, а объекты должны разделяться запятыми. Для удобства работы со стримом нужно либо ставить после запятых символ \n, либо делать это на стороне клиента с помощью sed:

curl ... | sed "s/},/}\n/g"

Каждый объект имеет поле type и опциональное поле data. Объекты с типом heartbeat посылаются несмотря ни на что один раз в пять секунд. Если клиент не видит такого объекта в течение десяти секунд, он считает, что либо что-то сломалось на стороне сервера, либо что-то не так с сетью, и закрывает соединение. Объект с типом sync используется в стримах, посылающих некое состояние, а затем обновления к нему, для разделения первого от второго. Наконец, все остальные типы представляют собой полезную нагрузку. Поле data нужно по той причине, что вложенные данные также могут иметь поле type, что приводило бы к неразберихе.

В-третьих, когда вы пишите RESTful приложение, старайтесь с самого начала придерживаться некоторых соглашений. Например, с самого начала договоритесь, что имена полей в JSON-объектах должны всегда писаться в camelCase. Раз и навсегда запретите использовать в идентификаторах такие спецсимволы, как знак плюс и пробелы. Договоритесь, что в случае получения кода 301 клиент должен посылать точно такой же запрос на URL, указанный в заголовке Location. Примите соглашение о том, как будет передаваться автоматически сгенерированные id. Например, в Riak для этого используется заголовок Location . Подумайте о том, как вы будете сообщать о различных типах ошибок, в том числе временной недоступности БД, ошибках валидации полей и так далее. Пользователи почти наверняка предпочтут увидеть:

{"message":"validation_error","description":"..."}

… вместо кода 500 без каких-либо дополнительных пояснений. Если для вашего приложения важна точность представления чисел, договоритесь передавать все числа в виде строк, чтобы json-декодер не терял точность из-за преобразования строк во float’ы.

Но помните, хотя все написанное выше — это идеал, к которому стоит стремиться, на практике всем наплевать на стандарты . А значит, вас ждет много подпорок, слепленных на скорую руку, нежелание коллег переходить на более правильные версии API (зачем, если все работает?), и многие другие увлекательные вещи.

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

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

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

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

Определение

Для начала нужно определиться, что же такое REST. Википедия даёт на этот вопрос следующий ответ. REST (Representational State Transfer - «передача состояния представления») - архитектурный стиль взаимодействия компонентов распределённого приложения в сети. REST представляет собой согласованный набор ограничений, учитываемых при проектировании распределённой гипермедиа-системы.

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

Также нужно понимать, что такое REST-сервис. Я бы дал определение REST-сервису, как “точка взаимодействия клиентского приложения с сервером”. Говоря Java терминологией - это сервлет, на который клиент посылает запрос.

Проблематика

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

Название сервиса

Для начала необходимо выбрать имя для REST сервиса. Под именем сервиса я подразумеваю его путь в URI запросе. Например, http://my-site.by/api/rest/service/name . Для выбора имени нам нужно понимать что такое “ресурсы” в архитектуре REST.

Представление ресурса

В терминологии REST что угодно может быть ресурсом - HTML-документ, изображение, информация о конкретном пользователе и т.д. Если ресурс представляет собой некоторый объект, его легко представить, используя некоторый стандартный формат, например, XML или JSON. Далее сервер может отправить данный ресурс, используя выбранный формат, а клиент сможет работать с полученным от сервера ресурсом, используя этот же формат.

Пример представления ресурса “профиль” в формате JSON:

    "id" :1 ,

    "name" :"Mahesh" ,

    "login" :"manesh"

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

  • Клиент и сервер должны “понимать” и иметь возможность работать с выбранным форматом.
  • Ресурс можно полностью описать, используя выбранный формат независимо от сложности ресурса.
  • Формат должен предусматривать возможность представления связей между ресурсами.

Пример представления ресурса “заказ” и его связи с ресурсом “профиль”:

    id: 11254 ,

    currency: "EUR" ,

    amount: 100 ,

    profile: {

    id: 11 ,

    uri: "http://MyService/Profiles/11"

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

Обращение к ресурсу

Каждый ресурс должен быть уникально обозначен постоянным идентификатором. «Постоянный» означает, что идентификатор не изменится за время обмена данными, и даже когда изменится состояние ресурса. Если ресурсу присваивается другой идентификатор, сервер должен сообщить клиенту, что запрос был неудачным и дать ссылку на новый адрес. Каждый ресурс однозначно определяется URL. Это значит, что URL по сути является первичным ключом для единицы данных. То есть, например, вторая книга с книжной полки будет иметь вид /books/2 , а 41 страница в этой книге - /books/2/pages/41 . Отсюда и получается строго заданный формат. Причем совершенно не имеет значения, в каком формате находятся данные по адресу /books/2/pages/41 – это может быть и HTML, и отсканированная копия в виде jpeg-файла, и документ Word.

Рекомендуется при определении имени REST-сервиса использовать имена ресурсов во множественном числе. Такой подход позволяет добавлять новые REST-сервисы лишь расширяя имена уже существующих. Например, сервис /books вернёт нам список всех книг, /books/3 вернёт информацию о 3-ей книге, а сервис /books/3/pages вернёт все страницы 3-ей книги.

Для сервисов, которые выполняют какие-то специфические действия над ресурсом, есть 2 подхода для указания действия: в имени сервиса или в его параметрах. Например, /books/3/clean или /books/3?clean . Я предпочитаю первый вариант, так как обычно такие сервисы не редко используют POST методы, которые не поддерживают передачу параметров в URl, что делает сервис, на мой взгляд, не очень читабельным. Используя определение типа действия в имени сервиса, мы делаем наш сервис более расширяемым, так как он не зависит от типа HTTP метода.

Также очень не рекомендуется использовать имена, включающие в себя несколько слов и описывающие бизнес составляющую сервиса (как это рекомендуется делать при именовании java методов). Например, вместо /getAllCars лучше сделать метод /cars . Если же метод нельзя никак описать одним словом, то необходимо применять единый стиль разделителей, я обычно использую ‘-’, что является наиболее популярным подходом. Например, /cars/3/can-sold.

Более подробно о проектировании названий REST-сервисов можно прочитать в

HTTP методы

В REST используются 4 основных HTTP метода: GET, POST, PUT, DELETE. В большинстве случаев каждый из методов служит для выполнения предопределённого ему действия из CRUD (c reate, r ead, u pdate, d elete - «создание, чтение, обновление, удаление» ).
POST - create, GET - read, PUT - update, DELETE - delete.

ВАЖНОЕ ДОПОЛНЕНИЕ: Существуют так называемые REST-Patterns, которые различаются связыванием HTTP-методов с тем, что они делают. В частности, разные паттерны по-разному рассматривают POST и PUT. Однако, PUT предназначен для создания, замены или обновления, для POST это не определено (The POST operation is very generic and no specific meaning can be attached to it). Поэтому иногда POST и PUT можно поменять местами. Но в большинстве случаев POST используют для создания, а PUT для редактирования, и чуть позже я объясню почему.

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

  • GET /books/ – получает список всех книг. Как правило, это упрощенный список, т.е. содержащий только поля идентификатора и названия объекта, без остальных данных.
  • GET /books/{id} – получает полную информацию о книге.
  • POST /books/ – создает новую книгу. Данные передаются в теле запроса.
    PUT /books/{id} – изменяет данные о книге с идентификатором {id}, возможно заменяет их. Данные также передаются в теле запроса.
  • OPTIONS /books – получает список поддерживаемых операций для указанного ресурса (практически не используется)
  • DELETE /books/{id} – удаляет данные с идентификатором {id}.

Безопасность и идемпотентность

Очень помогут в выборе HTTP метода знания о безопасности и идемпотентности этих методов.

Безопасный запрос - это запрос, который не меняет состояние приложения.

Идемпотентный запрос - это запрос, эффект которого от многократного выполнения равен эффекту от однократного выполнения.

Судя по данной таблице, GET-запрос не должен менять состояние ресурса, к которому применяется. PUT и DELETE запросы могут менять состояние ресурса, но их можно спокойно повторять, если нет уверенности, что предыдущий запрос выполнился. В принципе, это логично: если многократно повторять запрос удаления или замены определенного ресурса, то результатом будет удаление или замена ресурса. Но POST запрос, как мы видим из таблицы, небезопасный и неидемпотентный. То есть мало того, что он меняет состояние ресурса, так и многократное его повторение будет производить эффект, зависимый от количества повторений. Ему по смыслу соответствует операция добавления новых элементов в БД: выполнили запрос Х раз, и в БД добавилось Х элементов.

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

HTTP коды

В стандарте HTTP описано более 70 статус кодов. Хорошим тоном является использование хотя бы основных.

  • 200 – OK – успешный запрос. Если клиентом были запрошены какие-либо данные, то они находятся в заголовке и/или теле сообщения.
  • 201 – OK – в результате успешного выполнения запроса был создан новый ресурс.
  • 204 – OK – ресурс успешно удалён.
  • 304 – Not Modified – клиент может использовать данные из кэша.
  • 400 – Bad Request – запрос невалидный или не может быть обработан.
  • 401 – Unauthorized – запрос требует аутентификации пользователя.
  • 403 – Forbidden – сервер понял запрос, но отказывается его обработать или доступ запрещен.
  • 404 – Not found – ресурс не найден.
  • 500 – Internal Server Error – разработчики API должны стараться избежать таких ошибок.

Эти ошибки должны быть отловлены в глобальном catch-блоке, залогированы, но они не должны быть возвращены в ответе.

Чем обширнее набор кодов, который мы будем использовать, тем более понятный будет API, который мы создаём. Однако нужно учесть, что некоторые коды браузеры обрабатывают по-разному. Например, некоторые браузеры получив код ответа 307 сразу же выполняют редирект, а некоторые позволяют обработать такую ситуацию и отменить действие. Прежде чем использовать тот или иной код, необходимо полностью понимать, как он будет обрабатываться на клиентской стороне!

Headers

  • Content-Type - формат запроса;
  • Accept - список форматов ответа.

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

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

Фильтрация

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

Например, чтобы вывести все красные книги необходимо выполнить запрос:

GET /books?color=red

Сортировка

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

GET /books?sort=-year,+name

Пагинация

Для того, чтобы поддержать возможность загрузки списка ресурсов, которые должны отображаться на определённой странице приложения, в REST API должен быть предусмотрен функционал пагинации. Реализуется он с помощью знакомых нам по SQL параметрам limit и offset. Например:

GET /books?offset=10&limit=5

Помимо того хорошим тоном является вывод ссылок на предыдущую, следующую, первую и последнюю страницы в хидере Link. Например:

Link: ; rel="next",
; rel="last",
; rel="first",
; rel="prev"

Выбор полей ресурса

Для более удобного использования сервиса, для экономии трафика можно предоставить возможность управлять форматом вывода данных. Реализуется предоставлением возможности выбора полей ресурса, которые должен вернуть REST сервис. Например, если необходимо получить только id книг и их цвета, необходимо выполнить следующий запрос:

GET /books?fields=id,color

Хранение состояния

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

Пример сервиса, не хранящего состояние:
Request1:
Request2: GET http://MyService/Persons/2 HTTP/1.1

Каждый из этих запросов может быть обработан независимо от другого.

Пример сервиса, хранящего состояние:
Request1: GET http://MyService/Persons/1 HTTP/1.1
Request2: GET http://MyService/NextPerson HTTP/1.1

Чтобы обработать второй запрос, серверу потребуется “запомнить” id последнего человека, который был запрошен клиентом. Т.е. сервер должен “запомнить” свое текущее состояние, иначе второй запрос не может быть обработан. При проектировании сервиса, следует избегать необходимости в хранении состояния, так как это имеет ряд преимуществ.

Преимущества сервиса, не хранящего состояние:

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

Недостатки сервиса, не хранящего состояние:

  • клиент сам должен отвечать за передачу необходимого контекста сервису.

Версионность

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

  • С использованием Accept хидера. В данном случае версия API указывается в Accept - Accept:text/v2+json
  • С использованием URI. В таком подходе версия API указывается прямо в URI - http://localhost/api/v2/books
  • Использование кастомного хидера. Можно использовать собственный хидер, который будет отвечать только за передачу версии API - API-Version:v2
  • Использование параметра запроса. Можно использовать параметр запроса для передачи версии API - /books?v=2

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

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

Для удобного пользования нашими REST сервисами нужно создать хорошую и понятную документацию. Для этих целей можно использовать различные инструменты, например, Mashape или Apiary, но я рекомендую использовать Swagger.

Swagger - это технология, которая позволяет документировать REST-сервисы. Swagger поддерживает множество языков программирования и фреймворков. Плюс, Swagger предоставляет UI для просмотра документации.

Получить более подробную информацию о Swagger можно по данной .

Архивирование

Кэширование

Также для сокращения запросов к БД и увеличения быстродействия наших REST сервисов рекомендуется применить механизм кэширования. Кэширование можно настраивать как на уровне сервера, так и в самом приложении, в зависимости от ситуации.

Кэшированием можно управлять используя следующие HTTP заголовки:

  • Date - дата и время создания ресурса.
  • Last Modified - дата и время последнего изменения ресурса на сервере.
  • Cache-Control - заголовок HTTP 1.1 используемый для управления кэшированием.
  • Age - время, прошедшее с момента последнего получения ресурса, заголовок может быть добавлен промежуточным (между клиентом и сервером) компонентом (например, прокси сервер)

В данной заметке пример самого простого REST API на PHP без использования какого-либо фреймворка и других средств. Целью есть предоставить общую картину - как это все работает.
Недавно я уже опубликовал , в которой описан процесс создания REST API для проекта на Yii2 .

Т.к. никакие фреймворки с маршрутизаторами в примере использоваться не будут, нужно начать с перенаправления всех запросов на "точку входа" - файл index.php. Для сервера на Apache это можно сделать в файле.htaccess который должен располагаться в корне проекта:
Options +FollowSymLinks IndexIgnore */* RewriteEngine on # Перенаправление с ДОМЕН на ДОМЕН/api RewriteCond %{REQUEST_URI} ^/$ RewriteRule ^(.*)$ /api/$1 #Если URI начинается с api/ то перенаправлять все запросы на index.php RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^api/(.*)$ /index.php
Согласно правил, ссылка должна начинаться на /api и,например, для API работающего с таблицей users должна иметь такой вид:
ДОМЕН/api/users

Пример файла index.php
run(); } catch (Exception $e) { echo json_encode(Array("error" => $e->getMessage())); }
Как видно из кода - будем работать с объектом usersApi , т.е. с пользователями (таблица users). Т.к. для простоты примера я не использую тут Composer или другой механизм для автозагрузки классов, просто подключим файл класса с помощью
require_once "UsersApi.php";
Кроме пользователей, может потребоваться сделать api и для других сущностей, поэтому все классы различных API должны иметь один общий костяк, который будет определять метод запроса, действие для выполнения и тд. Создаем файл Api.php c абстрактным классом Api :
requestUri = explode("/", trim($_SERVER["REQUEST_URI"],"/")); $this->requestParams = $_REQUEST; //Определение метода запроса $this->method = $_SERVER["REQUEST_METHOD"]; if ($this->method == "POST" && array_key_exists("HTTP_X_HTTP_METHOD", $_SERVER)) { if ($_SERVER["HTTP_X_HTTP_METHOD"] == "DELETE") { $this->method = "DELETE"; } else if ($_SERVER["HTTP_X_HTTP_METHOD"] == "PUT") { $this->method = "PUT"; } else { throw new Exception("Unexpected Header"); } } } public function run() { //Первые 2 элемента массива URI должны быть "api" и название таблицы if(array_shift($this->requestUri) !== "api" || array_shift($this->requestUri) !== $this->apiName){ throw new RuntimeException("API Not Found", 404); } //Определение действия для обработки $this->action = $this->getAction(); //Если метод(действие) определен в дочернем классе API if (method_exists($this, $this->action)) { return $this->{$this->action}(); } else { throw new RuntimeException("Invalid Method", 405); } } protected function response($data, $status = 500) { header("HTTP/1.1 " . $status . " " . $this->requestStatus($status)); return json_encode($data); } private function requestStatus($code) { $status = array(200 => "OK", 404 => "Not Found", 405 => "Method Not Allowed", 500 => "Internal Server Error",); return ($status[$code])?$status[$code]:$status; } protected function getAction() { $method = $this->method; switch ($method) { case "GET": if($this->requestUri){ return "viewAction"; } else { return "indexAction"; } break; case "POST": return "createAction"; break; case "PUT": return "updateAction"; break; case "DELETE": return "deleteAction"; break; default: return null; } } abstract protected function indexAction(); abstract protected function viewAction(); abstract protected function createAction(); abstract protected function updateAction(); abstract protected function deleteAction(); }
Осталось реализовать абстрактные методы и свойство $apiName , которое уникально для каждого отдельного API. Для этого создаем файл UsersApi.php :
getConnect(); $users = Users::getAll($db); if($users){ return $this->response($users, 200); } return $this->response("Data not found", 404); } /** * Метод GET * Просмотр отдельной записи (по id) * http://ДОМЕН/users/1 * @return string */ public function viewAction() { //id должен быть первым параметром после /users/x $id = array_shift($this->requestUri); if($id){ $db = (new Db())->getConnect(); $user = Users::getById($db, $id); if($user){ return $this->response($user, 200); } } return $this->response("Data not found", 404); } /** * Метод POST * Создание новой записи * http://ДОМЕН/users + параметры запроса name, email * @return string */ public function createAction() { $name = $this->requestParams["name"] ?? ""; $email = $this->requestParams["email"] ?? ""; if($name && $email){ $db = (new Db())->getConnect(); $user = new Users($db, [ "name" => $name, "email" => $email ]); if($user = $user->saveNew()){ return $this->response("Data saved.", 200); } } return $this->response("Saving error", 500); } /** * Метод PUT * Обновление отдельной записи (по ее id) * http://ДОМЕН/users/1 + параметры запроса name, email * @return string */ public function updateAction() { $parse_url = parse_url($this->requestUri); $userId = $parse_url["path"] ?? null; $db = (new Db())->getConnect(); if(!$userId || !Users::getById($db, $userId)){ return $this->response("User with id=$userId not found", 404); } $name = $this->requestParams["name"] ?? ""; $email = $this->requestParams["email"] ?? ""; if($name && $email){ if($user = Users::update($db, $userId, $name, $email)){ return $this->response("Data updated.", 200); } } return $this->response("Update error", 400); } /** * Метод DELETE * Удаление отдельной записи (по ее id) * http://ДОМЕН/users/1 * @return string */ public function deleteAction() { $parse_url = parse_url($this->requestUri); $userId = $parse_url["path"] ?? null; $db = (new Db())->getConnect(); if(!$userId || !Users::getById($db, $userId)){ return $this->response("User with id=$userId not found", 404); } if(Users::deleteById($db, $userId)){ return $this->response("Data deleted.", 200); } return $this->response("Delete error", 500); } } Методы связанные с базой данных и получением данных из нее просто для примера.