REST API-дизайн ресурса, свойства которого не редактируются клиентом

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

Примеры:

  • Запрос нового токена, используемого для X. Маркер должен быть сгенерирован после определенного набора бизнес-правил/логики.

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

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

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

Давайте посмотрим на № 1 более подробно:

Запрос

GET /User/1

ответ

{
   "Id": 1,
   "Email": "[email protected]",
   "SpecialToken": "12345689"
}

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

Как сообщить API, что мне нужна новая/обновленная SpecialToken с помощью парадигмы REST?

Можно подумать:

Запрос

PATCH /User/1
{
   "SpecialToken": null
}

Сервер увидит этот запрос и узнает, что ему нужно обновить токен. Бэкэнд обновит SpecialToken специальным алгоритмом и вернет обновленный ресурс:

ответ

{
   "Id": 1,
   "Email": "[email protected]",
   "SpecialToken": "99999999"
}

Этот пример можно расширить до примера №2, где SpecialToken - это обменный курс на ресурсе CurrencyTrade. ExchangeRate - это значение только для чтения, которое потребитель API не может изменить напрямую, но может потребовать его изменения/обновления:

Запрос

GET /CurrencyTrade/1

ответ

{
   "Id": 1,
   "PropertyOne": "Value1",
   "PropertyTwo": "Value2",
   "ExchangeRate":  1.2
}

Кому-то, потребляющему API, потребуется способ запросить новый ExchangeRate, но они не имеют контроля над тем, какое значение будет иметь значение, это строго a read only property.

Ответ 1

Вы действительно имеете дело с двумя различными представлениями ресурса: один для того, что клиент может отправлять через POST/PUT, и один для того, что сервер может вернуть. Вы не имеете дело с самим ресурсом.

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

Если бы не было требования, которое его запрещало, я бы, вероятно, использовал сценарий генерации токена, "касаясь" представления ресурсов с помощью PUT. Предположительно, клиент не может обновить поле Id, поэтому он не будет определен в представлении клиента.

Запрос

PUT /User/1 HTTP/1.1
Content-Type: application/vnd.example.api.client+json

{
   "Email": "[email protected]"
}

ответ

200 OK
Content-Type: application/vnd.example.api.server+json

{
   "Id": 1,
   "Email": "[email protected]",
   "SpecialToken": "99999999"
}

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

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

Ответ 2

Есть много подходов к этому. Я бы сказал, что лучший из них, вероятно, имеет ресурс /User/1/SpecialToken, который дает 202 Accepted сообщение с объяснением того, что ресурс не может быть полностью удален и будет обновляться всякий раз, когда кто-то пытается это сделать. Затем вы можете сделать это с помощью DELETE, с PUT, который заменяет его нулевым значением и даже с помощью PATCH непосредственно на SpecialToken или на атрибут User. Несмотря на то, что кто-то еще упоминал, нет ничего плохого в сохранении значения SpecialToken в ресурсе пользователя. Клиенту не нужно будет выполнять два запроса.

Подход, предложенный @AndyDennie, POST для ресурса TokenRefresher, также прекрасен, но я предпочел бы другой подход, потому что он чувствует себя не так, как настроенное поведение. Как только в вашей документации будет ясно, что этот ресурс не может быть удален, и сервер просто обновляет его, клиент знает, что он может удалить или установить его в null с любым стандартизованным действием, чтобы обновить его.

Имейте в виду, что в реальном API RESTful гипермедиа-представление пользователя просто имеет ссылку с надписью "refresh token", с любой операцией, и семантика URI не имеет большого значения.

Ответ 3

Я считаю, что вы должны подумать о создании ресурса SpecialToken и разрешить потребителям api POST ему получить новый экземпляр. Так или иначе, вы захотите связать ресурс User с ресурсом SpecialToken. Помните, что одним из центральных принципов REST является то, что вы не должны зависеть от внеполосной информации, поэтому, если вы хотите оставаться верным этому, вам нужно исследовать возможность использования ссылок.

Во-первых, давайте посмотрим, что у вас есть:

Запрос:

GET /User/1
Accept: application/json

Ответ:

200 OK
Content-Type: application/json


{
   "Id": 1,
   "Email": "[email protected]",
   "SpecialToken": "12345689"
}

Хотя этот ответ включает свойство SpecialToken в объекте, поскольку Content-Type - application/json фактически не означает ничего для клиентов, которые не запрограммированы для понимания этой конкретной структуры объекта. Клиент, который просто понимает JSON, воспринимает это как объект, как любой другой. Пусть на этот раз игнорировать. Давайте просто скажем, что мы идем с идеей использования другого ресурса для поля SpecialToken; он может выглядеть примерно так:

Запрос:

GET /User/1/SpecialToken
Accept: application/json

Ответ:

200 OK
Content-Type: application/json

{
    "SpecialToken": "12345689"
}

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

Запрос:

POST /User/1/SpecialToken
Accept: application/json

Ответ:

200 OK
Content-Type: application/json

{
    "SpecialToken": "98654321"
}

Обратите внимание, что сообщение POST не содержит тело. Это может показаться нетрадиционным, но спецификация HTTP не запрещает это, и на самом деле TAG W3C говорит все в порядке:

Обратите внимание, что можно использовать POST даже без предоставления данных в теле сообщения HTTP. В этом случае ресурс адресуется URI, но метод POST указывает клиентам, что взаимодействие небезопасно или может иметь побочные эффекты.

Звучит о себе. В тот же день я слышал, что у некоторых серверов были проблемы с сообщениями POST без тела, но у меня лично не было проблем с этим. Просто убедитесь, что заголовок Content-Length установлен соответствующим образом, и вы должны быть золотыми.

Таким образом, имея в виду это, кажется, это совершенно правильный способ (согласно REST) ​​делать то, что вы предлагаете. Однако помните, прежде чем я упоминал о битах о JSON, на самом деле не имеющем семантики уровня приложения? Это означает, что для того, чтобы ваш клиент действительно отправил POST, чтобы получить новый SpecialToken, в первую очередь, ему нужно знать URL-адрес этого ресурса или, по крайней мере, как создать такой URL-адрес. Это считается плохой практикой, поскольку она связывает клиента с сервером. Проиллюстрируем.

Учитывая следующий запрос:

POST /User/1/SpecialToken
Accept: application/json

Если сервер больше не распознает URL /User/1/SpecialToken, он может вернуть 404 или другое соответствующее сообщение об ошибке, и ваш клиент теперь сломан. Чтобы исправить это, вам нужно будет изменить код, ответственный. Это означает, что ваш клиент и сервер не могут развиваться независимо друг от друга, и вы ввели связь. Исправление этого, однако, может быть относительно простым, если ваши клиентские HTTP-процедуры позволяют вам проверять заголовки. В этом случае вы можете ввести ссылки на свои сообщения. Вернемся к нашему первому ресурсу:

Запрос:

GET /User/1
Accept: application/json

Ответ:

200 OK
Content-Type: application/json
Link: </User/1/SpecialToken>; rel=token

{
   "Id": 1,
   "Email": "[email protected]",
   "SpecialToken": "12345689"
}

Теперь в ответе есть ссылка, указанная в заголовках. Это небольшое дополнение означает, что ваш клиент больше не должен знать, как добраться до ресурса SpecialToken, он может просто следовать по ссылке. Хотя это не заботится обо всех проблемах связи (например, token не является зарегистрированным отношением ссылки), он длится долго путь. Теперь ваш сервер может изменить URL SpecialToken по своему желанию, и ваш клиент будет работать без изменения.

Это небольшой пример HATEOAS, сокращенный для Hypermedia As Engine of Application State, что по сути означает, что ваше приложение обнаруживает, как делать что-то, а не знать их впереди. Кто-то из отдела аббревиатуры действительно уволился за это. Чтобы намочить ваш аппетит на эту тему, действительно крутой разговор Джона Мура, который показывает API, который широко использует гипермедиа. Еще одно приятное введение в гипермедиа - это записи Стива Клабника. Это должно заставить вас начать.

Надеюсь, это поможет!

Ответ 4

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

Запрос

GET /User/1
Accept: application/hal+json

ответ

200 OK
Content-Type: application/hal+json

{
   _links: {
     self: { href: "/User/1" },
     "token-revocation": { href: "/User/1/RevokedTokens" }
   },
   "Id": 1,
   "Email": "[email protected]",
   "SpecialToken": "12345689"
}

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

Запрос

POST /User/1/RevokedTokens
Content-Type: text/plain

123456789

ответ:   202 Принято (или 204 Нет содержимого)

Последующее GET для пользователя будет иметь новый специальный токен, назначенный ему:

Запрос

GET /User/1
Accept: application/hal+json

ответ

200 OK
Content-Type: application/hal+json

{
   _links: {
     self: { href: "/User/1" },
     "token-revocation": { href: "/User/1/RevokedTokens" }
   },
   "Id": 1,
   "Email": "[email protected]",
   "SpecialToken": "99999999"
}

Это имеет преимущество в моделировании реального ресурса (списка аннулирования токенов), который может влиять на другие ресурсы, а не на моделирование службы в качестве ресурса (то есть ресурса повторной передачи токенов).

Ответ 5

Как насчет отдельного ресурса, который отвечает за обновление токена в ресурсе пользователя?

POST /UserTokenRefresher
{
    "User":"/User/1"
}

Это может вернуть обновленное представление пользователя (с новым токеном) в ответе.