Проектирование веб-API RESTFUL

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

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

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

В этом руководстве описываются проблемы, которые следует учитывать при разработке веб-API.

Что такое REST?

В 2000 году Рой Филдинг предложил архитектурный подход к разработке веб-служб — передачу репрезентативного состояния (REST). REST — это архитектурная концепция создания распределенных систем на основе гиперсред. Модель REST не зависит от каких-либо базовых протоколов и не требует привязки к HTTP. Однако наиболее распространенные реализации REST API используют HTTP в качестве протокола приложения. Поэтому это руководство посвящено разработке REST API для HTTP.

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

Ниже приведены некоторые основные принципы проектирования интерфейсов RESTful API, использующих протокол HTTP:

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

  • У ресурса есть идентификатор (универсальный код ресурса (URI)), который уникально идентифицирует этот ресурс. Например, URI для определенного клиентского заказа может быть таким:

    https://adventure-works.com/orders/1
    
  • Клиенты взаимодействуют со службой путем обмена представлениями ресурсов. Многие веб-API используют JSON в качества формата для обмена. Например, в результате запроса GET к приведенному выше URI может вернуться такой текст ответа:

    {"orderId":1,"orderValue":99.90,"productId":1,"quantity":1}
    
  • Интерфейсы REST API используют единый интерфейс, который позволяет отделить реализации клиента и службы. Для REST API, созданных на основе протокола HTTP, единый интерфейс будет использовать стандартные HTTP-команды для выполнения операций с ресурсами. Наиболее часто выполняемые операции: GET, POST, PUT, PATCH и DELETE.

  • REST API использует модель запросов без отслеживания состояния. HTTP-запросы должны быть независимыми и могут возникать в любом порядке, поэтому сохранение временных сведений о состоянии между запросами невозможно. Сведения хранятся только в самих ресурсах, и каждый запрос должен быть атомарной операцией. Благодаря этому ограничению веб-службы имеют высокую масштабируемость, так как нет необходимости сохранять сходство между клиентами и конкретными серверами. Каждый сервер может обрабатывать любой запрос от любого клиента. Тем не менее, другие факторы могут ограничить масштабируемость. Например, многие веб-службы записываются в внутреннее хранилище данных, что может быть трудно масштабировать. Дополнительные сведения о стратегиях масштабирования хранилища данных см. в разделе "Горизонтальное, вертикальное и функциональное секционирование данных".

  • Интерфейсами REST API управляют ссылки на гиперсреды, которые содержатся в представлении. Например, ниже показано JSON-представление заказа. Оно содержит ссылки для получения или обновления клиента, связанного с заказом.

    {
      "orderID":3,
      "productID":2,
      "quantity":4,
      "orderValue":16.60,
      "links": [
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"GET" },
        {"rel":"product","href":"https://adventure-works.com/customers/3", "action":"PUT" }
      ]
    }
    

В 2008 году Леонард Ричардсон предложил следующую модель зрелости для веб-API:

  • Уровень 0. Определение одного URI, и все операции будут POST-запросами к этому URI.
  • Уровень 1. Создание отдельных URI для отдельных ресурсов.
  • Уровень 2. Использование методов HTTP для определения операций с ресурсами.
  • Уровень 3. Использование гиперсред (HATEOAS, описан ниже).

Уровень 3 соответствует настоящему RESTful API, согласно определению Филдинга. На практике многие опубликованные веб-API находятся где-то около уровня 2.

Организация проектирования API с учетом ресурсов

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

https://adventure-works.com/orders // Good

https://adventure-works.com/create-order // Avoid

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

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

https://adventure-works.com/orders

В результате отправки HTTP-запроса GET на URI коллекции возвращается список элементов в коллекции. Каждый элемент в коллекции также имеет свой собственный универсальный код ресурса (URI). HTTP-запрос GET к URI элемента возвращает подробные сведения об этом элементе.

Используйте единое соглашение об именовании в универсальных кодах ресурсов. В целом это помогает использовать существительные во множественном числе для URI, ссылающихся на коллекции. Рекомендуется упорядочивать универсальные коды ресурсов для коллекций и элементов в иерархии. Например, /customers — это путь к коллекции клиентов, а /customers/5 — путь к клиенту с идентификатором равным 5. Этот подход позволяет обеспечивать простоту веб-API. Кроме того, множество платформ веб-API могут направлять запросы на основании параметризованных путей URI, поэтому можно определить маршрут для пути /customers/{id}.

Также следует продумать связи между разными типами ресурсов и способы предоставления этих связей. Например, /customers/5/orders может представлять все заказы для клиента 5. Вы также можете пойти в другом направлении и представить связь между заказом и конкретным клиентом посредством универсального кода ресурса, например /orders/99/customer. Однако чрезмерное расширение этой модели может вызвать трудности. Более рациональное решение — предоставить ссылки с возможностью перехода на связанные ресурсы в тексте HTTP-ответа. Этот механизм более подробно описан в разделе об использовании принципа HATEOAS для реализации перехода к связанным ресурсам.

В более сложных системах очевидным решением может показаться предоставление URI, позволяющих клиенту переходить между несколькими уровнями связей, например /customers/1/orders/99/products. Однако такой уровень сложности трудно обслуживать и адаптировать в случае дальнейшего изменения связей между ресурсами. Вместо этого постарайтесь сделать URI максимально простыми. Как только у приложения появляется ссылка на ресурс, оно может использовать эту ссылку для поиска элементов, связанных с указанным ресурсом. Предыдущий запрос можно заменить на URI /customers/1/orders, чтобы найти все заказы для клиента 1, а затем — на /orders/99/products, чтобы найти продукты в этом заказе.

Совет

Старайтесь использовать универсальные коды ресурсов не сложнее collection/item/collection.

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

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

Наконец, сопоставление каждой операции, реализованной веб-API, с конкретным источником не всегда возможно. Такие безресурсные сценарии можно обрабатывать с помощью HTTP-запросов, вызывающих определенную функцию и возвращающих результаты в виде ответного HTTP-сообщения. Например, веб-API, реализующий простые расчетные операции, такие как добавление и вычитание, может предоставить универсальные коды ресурсов (URI), представляющие эти операции в виде псевдоресурсов, и использовать строку запроса для указания требуемых параметров. Например, запрос GET к URI /add?operand1=99&operand2=1 вернет сообщение ответа с текстом, содержащим значение 100. Однако следует использовать эти формы универсальных кодов ресурсов (URI) с осторожностью.

Определение операций API с точки зрения методов HTTP

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

  • GET. Возвращает представление ресурса по указанному универсальному коду ресурса (URI). Текст ответного сообщения содержит сведения о запрашиваемом ресурсе.
  • POST. Создает новый ресурс по указанному URI. Текст запроса содержит сведения о новом ресурсе. Обратите внимание, что метод POST также можно использовать для запуска операций, не относящихся непосредственно к созданию ресурсов.
  • PUT. Создает или заменяет ресурсы по указанному URI. В тексте сообщения запроса указан создаваемый или обновляемый ресурс.
  • PATCH. Выполняет частичное обновление ресурса. Текст запроса определяет набор изменений, применяемых к ресурсу.
  • DELETE. Удаляет ресурс по указанному URI.

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

Ресурс POST GET PUT DELETE
/customers Создание нового клиента Получение всех клиентов Массовое обновление клиентов Удаление всех клиентов
/customers/1 Ошибка Получение сведений для клиента 1 Обновление сведения о клиенте 1, если он существует Удаление клиента 1
/customers/1/orders Создание нового заказа для клиента 1 Получение всех заказов для клиента 1 Массовое обновление заказов для клиента 1 Удаление всех заказов для клиента 1

Различия между POST, PUT и PATCH могут запутать новичков.

  • Запрос POST создает ресурс. Сервер назначает URI для нового ресурса и возвращает этот URI клиенту. В модели REST запросы POST постоянно применяются к коллекциям. При этом новый ресурс добавляется в коллекцию. Запрос POST также может использоваться для отправки данных для обработки в имеющийся ресурс без создания нового ресурса.

  • Запрос PUT создает ресурс или обновляет имеющийся ресурс. Клиент указывает универсальный код ресурса. Текст запроса содержит полное представление ресурса. Если ресурс с таким URI уже существует, он заменяется. В противном случае создается новый ресурс, если сервер поддерживает это. Запросы PUT чаще всего применяются к ресурсам, которые являются отдельными элементами, например конкретные клиенты, а не к коллекциям. Сервер может поддерживать обновления, но не создание через PUT. Требуется ли поддерживать создание через PUT зависит от того, может ли клиент назначать информативный URI ресурсу до его существования. Если нет, используйте POST для создания ресурсов, а PUT или PATCH для обновления.

  • Запрос PATCH выполняет частичное обновление имеющегося ресурса. Клиент указывает универсальный код ресурса. Текст запроса определяет набор изменений, применяемых к ресурсу. Это может быть более эффективно, чем использование PUT, так как клиент отправляет только изменения, а не все представление ресурса. С технической точки зрения PATCH также может создать новый ресурс (указав набор обновлений для ресурса "null"), если это поддерживается сервером.

Запросы PUT должны быть идемпотентными. Если клиент отправляет тот же запрос PUT несколько раз, результаты всегда должны быть одинаковыми (тот же ресурс будет изменяться с теми же значениями). Запросы POST и PATCH не будут гарантированно идемпотентными.

Соответствие семантике HTTP

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

Типы мультимедиа

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

В протоколе HTTP форматы задаются с помощью типов мультимедиа, также называемых типами MIME. Для не двоичных данных большинство веб-API поддерживают JSON (тип носителя = application/json) и, возможно, XML (тип носителя = application/xml).

Заголовок Content-Type в запросе или ответе определяет формат представления. Ниже приведен пример запроса POST, который включает в себя данные JSON:

POST https://adventure-works.com/orders HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Length: 57

{"Id":1,"Name":"Gizmo","Category":"Widgets","Price":1.99}

Если сервер не поддерживает данный тип мультимедиа, он возвращает код состояния HTTP 415 (неподдерживаемый тип носителя).

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

GET https://adventure-works.com/orders/2 HTTP/1.1
Accept: application/json

Если сервер не может соответствовать ни одному из перечисленных типов носителей, он должен вернуть код состояния HTTP 406 (недопустимо).

Методы GET

Успешное выполнение метода GET обычно возвращает код состояния HTTP 200 (ОК). Если ресурс не найден, метод должен вернуть код 404 (не найдено).

Если запрос выполнен, но нет текста ответа, включенного в HTTP-ответ, он должен вернуть код состояния HTTP 204 (нет содержимого); Например, операция поиска, которая не дает совпадений, может быть реализована с этим поведением.

Методы POST

Если метод POST создает новый ресурс, он возвращает код состояния HTTP 201 (создано). URI нового ресурса содержится в заголовке Location ответа. Текст ответа содержит представление ресурса.

Если метод выполняет определенную обработку, но не создает новый ресурс, он может вернуть код состояния HTTP 200 и содержать результат операции в тексте ответа. Кроме того, при отсутствии результатов для возврата метод может вернуть код состояния HTTP 204 (нет содержимого) без текста ответа.

Если клиент помещает недопустимые данные в запрос, сервер должен вернуть код состояния HTTP 400 (неверный запрос). Текст ответа может содержать дополнительные сведения об ошибке или ссылку на универсальный код ресурса (URI), по которому можно получить более подробную информацию.

Методы PUT

Если метод PUT создает новый ресурс, он возвращает код состояния HTTP 201 (создано), как и метод POST. Если метод обновляет имеющийся ресурс, он возвращает коды состояния 200 (ОК) или 204 (нет содержимого). В некоторых случаях обновить имеющийся ресурс невозможно. В этом случае рассмотрите возможность возврата кода состояния HTTP 409 (конфликт).

Рассмотрите возможность реализации массовых HTTP-операций PUT, поддерживающих пакетные обновления нескольких ресурсов в коллекции. В запросе PUT должен быть указан универсальный код ресурса (URI) коллекции, а текст запроса должен содержать сведения о ресурсах, которые требуется изменить. Такой подход помогает сократить избыточность и повысить производительность.

Методы PATCH

С помощью запроса PATCH клиент отправляет набор обновлений в имеющийся ресурс в виде документа с исправлениями. Сервер обрабатывает документ с исправлениями, чтобы выполнить обновление. Документ с исправлениями не описывает весь ресурс, а только набор применяемых изменений. Спецификация метода PATCH (RFC 5789) не определяет конкретный формат документов с исправлениями. Формат необходимо определить на основе типа мультимедиа в запросе.

Вероятно, наиболее распространенный формат данных для веб-API — JSON. Есть два основных формата исправлений на основе JSON, называемые исправлением JSON и исправлением со слиянием JSON.

Исправление со слиянием JSON несколько проще. Документ с исправлениями имеет ту же структуру, что и исходный ресурс JSON, однако включает только подмножество полей, которые необходимо изменить или добавить. Кроме того, поле можно удалить, указав null для значения поля в документе с исправлениями. (Это означает, что исправление со слиянием не подходит, если исходный ресурс может иметь явные значения null.)

Предположим, что исходный ресурс имеет следующее представление JSON:

{
    "name":"gizmo",
    "category":"widgets",
    "color":"blue",
    "price":10
}

Вот возможное исправление со слиянием JSON для этого ресурса:

{
    "price":12,
    "color":null,
    "size":"small"
}

Серверу дается указание обновить price, удалить color и добавить size. name и category не изменяются. Точные сведения об исправлении со слиянием JSON см. в документе RFC 7396. Тип носителя для исправления со слиянием JSON — application/merge-patch+json.

Исправление со слиянием не подходит, если исходный ресурс может содержать явные значения null, из-за смысла значения null в документе с исправлениями. Кроме того, в документе с исправлениями не указывается порядок, в котором сервер должен применять обновления. Это может иметь значение, в зависимости от данных и предметной области. Исправление JSON, определенное в RFC 6902, — более гибкое. Оно определяет изменения как последовательность выполняемых операций, в частности добавление, удаление, замена, копирование и тестирование (для проверки значений). Тип носителя для исправления JSON — application/json-patch+json.

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

Условие ошибки Код состояния HTTP
Формат документа с исправлениями не поддерживается. 415 (неподдерживаемый тип носителя)
Неправильный формат документа с исправлениями. 400 (недопустимый запрос)
Документ с исправлениями действителен, но изменения нельзя применить к ресурсу в его текущем состоянии. 409 (конфликт)

Методы DELETE

Если операция удаления прошла успешно, веб-сервер должен вернуть ответ с кодом состояния HTTP 204 (Содержимое отсутствует), указывающим на успешное завершение процесса и отсутствие дополнительных сведений в тексте ответа. Если ресурс не существует, веб-сервер может вернуть код HTTP 404 (не найдено).

Асинхронные операции

Иногда операциям POST, PUT, PATCH или DELETE может потребоваться обработка, которая занимает некоторое время. Если вы ожидаете завершения перед отправкой ответа клиенту, это может привести к неприемлемой задержке. В этом случае рассмотрите возможность создания асинхронной операции. Верните код состояния HTTP 202, указывающий, что запрос был принят в обработку, но не завершен.

Следует предоставить конечную точку, которая возвращает состояние асинхронного запроса, чтобы клиент мог отслеживать состояние, опрашивая конечную точку состояния. Включите URI конечной точки состояния в заголовок Location ответа 202. Например:

HTTP/1.1 202 Accepted
Location: /api/status/12345

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

HTTP/1.1 200 OK
Content-Type: application/json

{
    "status":"In progress",
    "link": { "rel":"cancel", "method":"delete", "href":"/api/status/12345" }
}

Если асинхронная операция создает новый ресурс, конечная точка состояния должна вернуть код состояния 303 (см. другие) после завершения операции. В ответе 303 включите заголовок Location, который предоставляет URI нового ресурса:

HTTP/1.1 303 See Other
Location: /api/orders/12345

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

Пустые наборы в телах сообщений

В любой момент, когда текст успешного ответа пуст, код состояния должен иметь значение 204 (нет содержимого). Для пустых наборов, таких как ответ на отфильтрованный запрос без элементов, код состояния по-прежнему должен быть 204 (нет содержимого), а не 200 (ОК).

Фильтрация и разбитие данных на страницы

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

Вместо этого API может разрешить передачу фильтра в строке запроса URI, например /orders?minCost=n. Веб-API отвечает за синтаксический анализ и обработку параметра minCost в строке запроса и возврат отфильтрованных результатов на стороне сервера.

Запросы GET по ресурсам коллекций потенциально могут возвращать большое число элементов. При проектировании веб-API следует ввести ограничение на объем данных, возвращаемый одним запросом. Рассмотрите возможность использования строк запроса, определяющих максимальное количество элементов для получения и начальное смещение в коллекции. Например:

/orders?limit=25&offset=50

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

Аналогичную стратегию для сортировки данных можно также применить при их получении. Вы можете указать параметр сортировки, использующий имя поля в качестве значения, например /orders?sort=ProductID. Однако такой подход может негативно отразиться на кэшировании (так как параметры строки запроса составляют часть идентификатора ресурса, используемого многими реализациями кэша в качестве ключа к кэшированным данным).

Вы можете расширить этот подход и ограничить возвращаемые поля для каждого элемента, если каждый элемент содержит большой объем данных. Например, используйте параметр строки запроса, принимающий разделенный запятыми список полей, например /orders?fields=ProductID,Quantity.

Присвойте содержательные значения по умолчанию всем необязательным параметрам в строках запроса. Например, установите параметру limit значение 10, а параметру offset — 0, если вы реализуете разбиение по страницам, параметру сортировки в качестве значения задайте ключ ресурса, если вы реализуете упорядочение, а в параметре fields укажите все поля в ресурсе при поддержке проекций.

Поддержка частичных ответов для больших двоичных ресурсов

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

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

HEAD https://adventure-works.com/products/10?fields=productImage HTTP/1.1

Вот пример сообщения ответа:

HTTP/1.1 200 OK

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 4580

Заголовок Content-Length содержит общий размер ресурса, а заголовок Accept-Ranges указывает, что соответствующая операция GET поддерживает частичные результаты. Клиентское приложение может использовать эту информацию для получения изображения небольшими фрагментами. Первый запрос возвращает первые 2500 байт с помощью заголовка "Range":

GET https://adventure-works.com/products/10?fields=productImage HTTP/1.1
Range: bytes=0-2499

Ответное сообщение указывает, что это частичный ответ, возвращая код состояния HTTP 206. Заголовок "Content-Length" указывает фактическое число возвращаемых байтов в тексте сообщения (не размер ресурса), а заголовок "Content-Range" указывает, какая это часть ресурса (байты 0–2499 из 4580):

HTTP/1.1 206 Partial Content

Accept-Ranges: bytes
Content-Type: image/jpeg
Content-Length: 2500
Content-Range: bytes 0-2499/4580

[...]

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

Одна из основных целей реализации модели REST — получение возможности перемещаться внутри всего набора ресурсов без предварительного знания схемы универсальных кодов ресурсов (URI). Каждый HTTP-запрос GET должен возвращать сведения, необходимые для поиска ресурсов, напрямую связанных с запрашиваемым объектом, посредством гиперссылок, включенных в ответ. Запросу GET также необходимо предоставить сведения, описывающие операции, доступные в каждом из этих ресурсов. Этот принцип называется HATEOAS (гипертекст как обработчик состояния приложения). Система фактически представляет собой конечный автомат. Ответ по каждому запросу содержит сведения, необходимые для перемещения между состояниями. Другие сведения не требуются.

Примечание.

На сегодняшний день не существует универсальных стандартов, определяющих правила моделирования принципа HATEOAS. Примеры в этом разделе демонстрируют одно из возможных проприетарных решений.

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

{
  "orderID":3,
  "productID":2,
  "quantity":4,
  "orderValue":16.60,
  "links":[
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"customer",
      "href":"https://adventure-works.com/customers/3",
      "action":"DELETE",
      "types":[]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"GET",
      "types":["text/xml","application/json"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"PUT",
      "types":["application/x-www-form-urlencoded"]
    },
    {
      "rel":"self",
      "href":"https://adventure-works.com/orders/3",
      "action":"DELETE",
      "types":[]
    }]
}

В этом примере в массиве links есть набор ссылок. Каждая ссылка представляет операцию в связанной сущности. Данные для каждой ссылки включают связь ("customer"), URI (https://adventure-works.com/customers/3), метод HTTP и поддерживаемые типы MIME. Это все данные, необходимые клиентскому приложению, чтобы иметь возможность вызова операции.

Массив links также включает автореферентные сведения, относящиеся к полученному ресурсу. Они имеют отношение self.

Набор возвращаемых ссылок может изменяться в зависимости от состояния ресурса. Вот почему гипертекст является "обработчиком состояния приложения".

Управление версиями веб-API с поддержкой REST

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

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

Отсутствие управления версиями

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

Например, запрос на универсальный код ресурса (URI) https://adventure-works.com/customers/3 должен вернуть сведения одного клиента с полями id, name и address, которые ожидает клиентское приложение.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Примечание.

Для простоты примеры ответов в этом разделе не содержат ссылок HATEOAS.

Если поле DateCreated добавить в схему ресурса клиента, ответ будет выглядеть следующим образом:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":"1 Microsoft Way Redmond WA 98053"}

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

Управление версиями через универсальные коды ресурсов (URI)

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

Расширим предыдущий пример. Если изменить структуру поля address и добавить подполя, содержащие каждую составную часть адреса (например, streetAddress, city, state и zipCode), эту версию ресурса можно предоставить посредством URI с номером версии, например https://adventure-works.com/v2/customers/3.

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

Этот механизм управления версиями очень прост, но для него требуется, чтобы сервер направлял запрос к соответствующей конечной точке. Однако этот подход может стать чрезмерно сложным по мере прохождения веб-API через несколько итераций и необходимости поддержки сервером нескольких различных версий. Кроме того, c пуристической точки зрения, во всех случаях клиентские приложения получают одни и те же данные (клиент 3), поэтому универсальный код ресурса (URI) фактически не должен отличаться в зависимости от версии. Эта схема также усложняет реализацию HATEOAS, поскольку потребуется включать номер версии в универсальные коды ресурсов (URI) всех ссылок.

Управление версиями через строку запроса

Чтобы не указывать множество кодов URI, вы можете указать версию ресурса с помощью параметра в строке запроса, добавленного к HTTP-запросу, например https://adventure-works.com/customers/3?version=2. Значение по умолчанию параметра версии должно быть содержательным, например 1, если оно опускается клиентскими приложениями прежних версий.

Этот подход обладает семантическим преимуществом в том плане, что один и тот же ресурс всегда возвращается по одинаковому универсальному коду ресурса (URI). Однако он зависит от кода, обрабатывающего запрос для анализа строки запроса и возврата соответствующего HTTP-ответа. Кроме того, этот подход также усложняет реализацию HATEOAS, как и механизм управления версиями через универсальные коды ресурсов (URI).

Примечание.

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

Управление версиями заголовка

Вместо того чтобы добавлять номер версии в виде параметра строки запроса, вы можете реализовать пользовательский заголовок, указывающий версию ресурса. Этот подход требует от клиентского приложения добавления соответствующего заголовка во все запросы. При этом в коде, обрабатывающем запрос клиентского приложения, можно использовать значение по умолчанию (версия 1), если заголовок с номером версии опускается. В следующих примерах используется пользовательский заголовок с именем Custom-Header. Значение этого заголовка указывает версию веб-API.

Версия 1:

GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Версия 2:

GET https://adventure-works.com/customers/3 HTTP/1.1
Custom-Header: api-version=2
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"id":3,"name":"Contoso LLC","dateCreated":"2014-09-04T12:11:38.0376089Z","address":{"streetAddress":"1 Microsoft Way","city":"Redmond","state":"WA","zipCode":98053}}

Как и в двух предыдущих подходах, для реализации HATEOAS потребуется включать соответствующий пользовательский заголовок во все ссылки.

Управление версиями через тип носителя

При отправке HTTP-запроса GET на веб-сервер клиентское приложение должно указывать поддерживаемый формат содержимого с помощью заголовка "Accept", как описано ранее в этом руководстве. Нередко заголовок Accept используется для того, чтобы позволить клиентскому приложению указать требуемый формат текста ответа: XML, JSON или другой формат, который клиентское приложение может анализировать. Однако можно определить пользовательские типы носителей, которые содержат сведения, позволяющие клиентскому приложению указывать предпочтительную версию ресурса.

В следующем примере показан запрос, в котором используется заголовок Accept со значением application/vnd.adventure-works.v1+json. Элемент vnd.adventure-works.v1 указывает веб-серверу на необходимость возврата ресурса версии 1, в то время как элемент json указывает JSON в качестве предпочтительного формата текста ответа.

GET https://adventure-works.com/customers/3 HTTP/1.1
Accept: application/vnd.adventure-works.v1+json

Код, обрабатывающий запрос, отвечает за обработку заголовка Accept и максимальное выполнение содержащихся в нем требований (клиентское приложение может указать в заголовке Accept несколько форматов, в случае чего веб-сервер может выбрать наиболее подходящий формат текста ответа). Веб-сервер подтверждает формат данных в тексте ответа с помощью заголовка "Content-Type":

HTTP/1.1 200 OK
Content-Type: application/vnd.adventure-works.v1+json; charset=utf-8

{"id":3,"name":"Contoso LLC","address":"1 Microsoft Way Redmond WA 98053"}

Если в заголовке "Accept" не указан ни один из известных типов носителя, веб-сервер может отправить ответное сообщение с кодом HTTP 406 (неприемлемо) или вернуть сообщение с типом носителя по умолчанию.

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

Примечание.

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

Механизмы управления версиями через заголовок и тип носителя, как правило, требуют дополнительной логики для проверки значений в пользовательском заголовке или заголовке "Accept". Использование многочисленными клиентами разных версий веб-API в крупномасштабной среде может привести к образованию большого объема повторяющихся данных в кэше на стороне сервера. Эта проблема может усложниться, если клиентское приложение обменивается данными с веб-сервером через прокси-сервер, реализующий кэширование, который перенаправляет запрос на веб-сервер лишь в том случае, если в его кэше на текущий момент не содержится копия запрашиваемых данных.

Open API Initiative

Организация Open API Initiative создана отраслевым консорциумом для стандартизации описаний REST API разных поставщиков. В рамках этой инициативы спецификация Swagger 2.0 была переименована в OpenAPI Specification (OAS) и перенесена в проект Open API Initiative.

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

  • Спецификация OpenAPI поставляется с набором заключений и рекомендаций по разработке REST API. Она дает ряд преимуществ для взаимодействия, но требует дополнительного внимания при разработке API для соблюдения спецификации.

  • В OpenAPI рекомендуется начинать работу с создания контракта, а не реализации. Это означает, что при разработке API вы сначала создаете контракт (интерфейс) и лишь после этого пишете код для его реализации.

  • При помощи Swagger и других средства можно создавать клиентские библиотеки и документацию на основе контрактов API. См. пример создания веб-API справки на ASP.NET с помощью Swagger.

Следующие шаги