Концепции REST API

10

У меня есть три вопроса о дизайне REST API, которые, я надеюсь, кто-то может пролить немного света. Я много часов искал, но нигде не нашел ответов на свои вопросы (может быть, я просто не знаю, что искать?).

Вопрос 1

Мой первый вопрос касается действий / RPC. Некоторое время я разрабатывал REST API, и я привык думать о вещах с точки зрения коллекций и ресурсов. Тем не менее, я столкнулся с парой случаев, когда парадигма, кажется, не применяется, и мне интересно, есть ли способ согласовать это с парадигмой REST.

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

Целесообразно ли смешивать какой-либо вызов действия с URI ресурса (например /collection/123?action=resendEmail)? Было бы лучше указать действие и передать ему идентификатор ресурса (например /collection/resendEmail?id=123)? Это неправильный путь? Традиционно (по крайней мере, с HTTP) выполняемое действие является методом запроса (GET, POST, PUT, DELETE), но в действительности они не допускают пользовательских действий с ресурсом.

вопрос 2

Я использую часть строки запроса в URL для фильтрации набора ресурсов, возвращаемых при запросе к коллекции (например /collection?someField=someval). Затем в моем контроллере API я определяю, какое сравнение он будет делать с этим полем и значением. Я обнаружил, что это действительно не работает. Мне нужен способ, позволяющий пользователю API указывать тип сравнения, который он хочет выполнить.

Лучшая идея , которую я придумал до сих пор , чтобы позволить пользователю API , чтобы указать его в качестве придатка к имени поля (например , /collection?someField:gte=someval- чтобы указать , что он должен возвращать ресурсы туда , где someFieldбольше или равно (> =) все , что somevalесть Является ли это хорошей идеей? Плохой идеей? Если да, то почему? Существует ли лучший способ, позволяющий пользователю указать тип сравнения для выполнения с данным полем и значением?

Вопрос 3

Я часто вижу URI, которые выглядят примерно так, /person/123/dogsчтобы получить persons dogs. Я обычно избегал чего-то подобного, потому что в конце я понимаю, что, создавая такой URI, вы на самом деле просто получаете доступ к dogsколлекции, отфильтрованной по определенному personидентификатору. Это было бы эквивалентно /dogs?person=123. Есть ли когда-нибудь веская причина для того, чтобы REST URI был больше двух уровней ( /collection/resource_id)?

Джастин Варкентин
источник
10
У тебя три вопроса. Почему бы не опубликовать их отдельно?
Анаксимандр
3
Было бы лучше разбить это на 3 отдельных вопроса. Зритель может быть в состоянии дать отличный ответ на один, но не на все вопросы.
2
Я думаю, что они все связаны. Название немного высокого уровня, но этот вопрос поможет многим людям, и его легко найти во время поиска SE. Этот вопрос должен стать вики-сообществом после того, как будет добавлено достаточно голосов. Мне потребовались недели, чтобы исследовать этот материал.
Эндрю Т Финнелл
1
Возможно, было бы лучше разместить их отдельно, ИДК. Однако, как отметил @AndrewFinnell, я подумал, что было бы неплохо объединить вопросы, поскольку это были самые сложные вопросы, связанные с REST, которые у меня были, и было бы неплохо, чтобы другие люди могли найти ответы на них. все вместе.
Джастин Варкентин

Ответы:

11

Целесообразно ли смешивать какой-либо вызов действия с URI ресурса (например /collection/123?action=resendEmail)? Было бы лучше указать действие и передать ему идентификатор ресурса (например /collection/resendEmail?id=123)? Это неправильный путь? Традиционно (по крайней мере, с HTTP) выполняемое действие является методом запроса (GET, POST, PUT, DELETE), но в действительности они не допускают пользовательских действий с ресурсом.

Я предпочел бы смоделировать это по-другому, с набором ресурсов, представляющих электронные письма, которые должны быть отправлены; отправка будет обработана внутренними службами в надлежащее время, после чего соответствующий ресурс будет удален. (Или пользователь может УДАЛИТЬ ресурс раньше, вызывая отмену запроса на отправку.)

Что бы вы ни делали, не помещайте глаголы в название ресурса! Это существительное (а часть запроса - это набор прилагательных). Существующие глаголы странники ОТДЫХ!

Я использую часть строки запроса в URL для фильтрации набора ресурсов, возвращаемых при запросе к коллекции (например /collection?someField=someval). Затем в моем контроллере API я определяю, какое сравнение он будет делать с этим полем и значением. Я обнаружил, что это действительно не работает. Мне нужен способ, позволяющий пользователю API указывать тип сравнения, который он хочет выполнить.

Наилучшая идея, которую я до сих пор предлагал, - позволить пользователю API указывать его в качестве дополнения к имени поля (например, /collection?someField:gte=somevalчтобы указать, что он должен возвращать ресурсы, где someField больше или равно ( >=) someval). Является ли это хорошей идеей? Плохой идеей? Если да, то почему? Существует ли лучший способ, позволяющий пользователю указать тип сравнения для выполнения с данным полем и значением?

Я бы предпочел указать общее предложение фильтра и использовать его в качестве необязательного параметра запроса для любого запроса для извлечения содержимого коллекции. Затем клиент может точно указать, как ограничить возвращаемый набор любым желаемым способом. Я бы также немного беспокоился об обнаружении языка фильтра / запроса; чем богаче вы его делаете, тем сложнее его обнаружить произвольным клиентам. Альтернативный подход, который, по крайней мере теоретически, имеет дело с этой проблемой обнаруживаемости, состоит в том, чтобы разрешить создание подресурсов ограничения коллекции, которые клиенты получают, размещая документ, описывающий ограничение ресурса коллекции. Это все еще небольшое злоупотребление, но, по крайней мере, это то, что вы можете явно обнаружить!

Такой вид обнаружения - одна из тех вещей, которые я нахожу наименее сильной в REST.

Я часто вижу URI, которые выглядят как /person/123/dogsсобачки. Я обычно избегал чего-то подобного, потому что в конце я понимаю, что, создавая такой URI, вы на самом деле просто получаете доступ к коллекции собак, отфильтрованной по определенному идентификатору человека. Это было бы эквивалентно /dogs?person=123. Есть ли когда-нибудь веская причина для того, чтобы REST URI был больше двух уровней ( /collection/resource_id)?

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

Другие типы коллекции могут быть смоделированы как перенаправление HTTP; таким образом, на него /person/123/dogsдействительно можно ответить, выполнив 307, на которую перенаправляется /dogs?person=123. В этом случае коллекция на самом деле не является композицией UML, а скорее агрегацией UML. Разница имеет значение; это важно!

Donal Fellows
источник
2
У вас есть солидные очки в целом. Однако, хотя resendEmailдействие можно обработать, создав коллекцию и разместив в ней POSTing, это кажется менее естественным. На самом деле я ничего не храню в базе данных, когда электронная почта отправляется повторно (нет необходимости). Ни один ресурс не изменяется, поэтому это просто действие, которое либо успешно, либо неудачно. Я не мог вернуть идентификатор ресурса, который существует за пределами срока действия вызова, что делает такую ​​реализацию хаком вместо того, чтобы быть RESTful. Это просто не операция CRUD.
Джастин Варкентин
3

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

Вы правы в том, что REST является системой сбора ресурсов. Это означает представительский государственный трансферт. Не очень хорошее определение, если вы спросите меня. Но основными понятиями являются 4 HTTP VERB и отсутствие состояния.

Важно отметить, что у вас есть только 4 VERBS с REST. Это GET, POST, PUT и DELETE. Ваш resendпример будет добавить новый глагол для REST. Это должен быть красный флаг.

Вопрос 1

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

У вас есть несколько вариантов.

REST - Подход, основанный на ресурсах задач

Создайте tasksресурс, в котором вы можете отправлять конкретные задачи в вашу систему для выполнения действий. Затем вы можете GETвыполнить задание на основе IDвозвращенного значения, чтобы определить его состояние.

Или вы можете смешать SOAP over HTTPвеб-сервис, чтобы добавить RPC в вашу архитектуру.

Запросы для всех задач для конкретного ресурса

GET http://server/api/myCollection/123/tasks

{ "tasks" :
    [ { "22333" : "http://server/api/tasks/223333" } ] 
}

пример ресурса задачи

PUT http://server/api/tasks

{ 
    "type" : "send-email" , 
    "parameters" : 
    { 
         "collection-type" : "foo" , 
         "collection-id" : "123" 
    } 
}

==> возвращает идентификатор задачи

223334

GET http://server/api/tasks/223334

{ 
    "status" : "complete" , 
    "date" : "whenever" 
}

REST - использование POST для запуска действий

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

Вы можете сделать POST похожим на это:

POST http://server/api/collection/123

{ "action" : "send-email" }

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

Проблема, с которой я столкнулся, заключается в том, GETчто ресурс на этом ресурсе вернет эти обновленные данные. Тем не менее, это решит ваши требования и все равно будет RESTful.

SOAP - веб-служба, которая принимает ресурсы, полученные от REST

Создайте новый WebService, в котором вы можете отправлять электронные письма на основе предыдущего идентификатора ресурса из REST API. Я не буду вдаваться в подробности о SOAP, поскольку первоначальный вопрос касается REST, и эти две концепции / технологии не следует сравнивать, так как это яблоки и апельсины .

вопрос 2

У вас также есть несколько вариантов здесь:

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

GET http://server/api/search?q="type = myCollection & someField >= someval"

Который возвратил бы коллекцию полностью определенных ресурсов REST, таких как:

{
    "results" : 
       { [ 
             "location" : "http://server/api/myCollection/1",
             "location" : "http://server/api/myCollection/9",
             "location" : "http://server/api/myCollection/56"
         ]
       }
}

Или вы можете разрешить что-то вроде MVEL в качестве параметра запроса.

Вопрос 3

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

Ноты

Я не согласен с читаемостью комментариев от других. Несмотря на то, что некоторые могут подумать, REST все еще не для потребления человеком. Это для машинного потребления. Если я хочу видеть свои твиты, я использую обычный веб-сайт Twitters. Я не выполняю REST GET с их API. Если я хочу программно что-то сделать со своими твитами, тогда я использую их REST API. Да, API должны быть понятными, но вы gteне так уж и плохи, просто они не интуитивно понятны.

Другая важная вещь в REST - это то, что вы должны иметь возможность начинать с любого заданного пункта в вашем API и переходить ко всем другим связанным ресурсам, БЕЗ знания точного URL других ресурсов заранее. Результаты GETVERB в REST должны возвращать полный REST URL ресурсов, на которые он ссылается. Таким образом, вместо запроса, возвращающего идентификатор Personобъекта, он возвращает полный URL-адрес, такой как http://server/api/people/13. Тогда вы всегда можете программно перемещаться по результатам, даже если URL изменился.

Ответ на комментарий

На самом деле в реальности существуют вещи, которые не должны создавать, читать, обновлять или удалять (CRUD) ресурс.

Дополнительные действия могут быть предприняты на ресурсах. Типичные реляционные базы данных поддерживают концепцию хранимых процедур. Это дополнительные команды, которые могут быть выполнены для набора данных. REST по своей сути не имеет этой концепции. И нет причин, по которым это следует. Эти типы действий идеально подходят для RPC или SOAP Web Services.

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

У вас есть несколько вариантов:

  • Создать ресурс задачи
  • Поддержка POSTдополнительных данных к ресурсу для выполнения действия.
  • Добавьте дополнительные команды через веб-службу SOAP.

Если бы вы использовали параметр запроса, какой HTTP VERB вы бы использовали для повторной отправки электронной почты?

  • GET- Повторно ли отправляется электронное письмо И возвращаются ли данные ресурса? Что если система кэширует этот URL и обрабатывает его как уникальный URL для этого ресурса. Каждый раз, когда они нажимают на URL, они пересылают письмо.
  • POST - На самом деле вы не отправляли новые данные на ресурс, только дополнительный параметр запроса.

Исходя из всех данных требований, решение проблемы POSTс ресурсом с action fieldданными POST.

Эндрю Т Финнелл
источник
3
Хотя REST, реализованный через HTTP, дает вам эти 4 глагола, я не уверен, что эти глаголы должны быть концом этого. На самом деле в реальности существуют вещи, которые не должны создавать, читать, обновлять или удалять (CRUD) ресурс. Повторная отправка электронной почты является одной из таких вещей. Мне не нужно ничего хранить или изменять в базе данных. Это просто действие, которое либо успешно, либо неудачно.
Джастин Варкентин
@JustinWarkentin Я понимаю, каковы ваши потребности. Но это не делает REST чем-то, что это не так. Добавление нового глагола в URL противоречит архитектуре REST. Я обновлю свой ответ, чтобы предложить другую альтернативу, которая будет RESTful.
Эндрю Т Финнелл
@JustinWarkentin Проверьте «ОТДЫХ - Использование POST для запуска действий» в моем ответе.
Эндрю Т Финнелл
0

Вопрос 1: Целесообразно ли смешивать какой-либо вызов действия с URI ресурса [или], лучше ли указать действие и передать ему идентификатор ресурса?

Хороший вопрос. В этом случае я бы посоветовал вам использовать последний подход, а именно указать действие и передать ему идентификатор ресурса. Таким образом, когда ваш ресурс впервые изменяется, он, в свою очередь, вызывает /sendEmailдействие (примечание: не нужно называть его «повторно») как отдельный запрос RESTful (который вы можете позже вызывать снова и снова, независимо от того, какой ресурс изменен). ).

Вопрос 2: относительно использования оператора сравнения, например, так:/collection?someField:gte=someval

Хотя это технически нормально, это, вероятно, плохая идея. Одним из ключевых принципов REST является удобочитаемость. Я бы посоветовал вам просто передать оператор сравнения в качестве другого параметра, например: /collection?someField=someval&operator=gteи, конечно, спроектировать свой API так, чтобы он обслуживал случай по умолчанию (в случае, если operatorпараметр не указан в URI).

Вопрос 3: Есть ли когда-нибудь веская причина для того, чтобы REST URI имел глубину более двух уровней?

Ага; для абстракции. Я видел пару REST API, которые используют уровни абстракции через несколько уровней URI, например: /vehicles/cars/123или /vehicles/bikes/123которые, в свою очередь, позволяют работать с полезной информацией, относящейся как к коллекциям, так /vehiclesи к /vehicles/bikesколлекциям. Сказав это, я не большой поклонник этого подхода; вам редко придется делать это на практике, и есть вероятность, что вы могли бы перепроектировать API, чтобы использовать только 2 уровня.

И да, как показывают комментарии выше, в будущем было бы лучше разделить ваши вопросы на отдельные посты;)

Коста Контос
источник
Я думаю, что мой пример для вопроса № 2 был слишком упрощенным. Мне нужно указать оператор сравнения для каждого поля, используемого для фильтрации коллекции, а не только для одного, поэтому в вашем примере это должно быть что-то вроде /collection?field1=someval&field1Operator=gte&field2=someval&field2Operator=eq.
Джастин Варкентин
0

Для вопроса 2, другая альтернатива может быть более гибкой: рассмотрите каждый поиск ресурс, который пользователь создает перед использованием.

Допустим, у вас есть «поисковый» контейнер, там вы делаете POST /api/searches/со спецификацией запроса на контент. это может быть JSON, XML или даже документ SQL, что вам проще. Если запрос анализируется правильно, новый поиск создается как новый ресурс с собственным URI, скажем,/api/searches/q123/

Затем клиент может просто GET /api/searches/q123/получить результаты запроса.

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

Хавьер
источник
0

Целесообразно ли смешивать какой-либо вызов действия с URI ресурса (например, / collection / 123? Action = resendEmail)? Было бы лучше указать действие и передать ему идентификатор ресурса (например, / collection / resendEmail? Id = 123)? Это неправильный путь? Традиционно (по крайней мере, с HTTP) выполняемое действие является методом запроса (GET, POST, PUT, DELETE), но в действительности они не допускают пользовательских действий с ресурсом.

Нет, это не подходит, так как IRI предназначены для идентификации ресурсов, а не операций (однако ppl некоторое время использует этот метод переопределения методов , в случаях, когда использование не POST и GET методов не поддерживается). Что вы можете сделать, так это найти подходящий метод HTTP или создать новый. POST может быть вашим другом в этих случаях (ppl использует его, если они не могут найти подходящий метод и запрос не является поисковым). Еще один подход к получению ресурсов от отправки электронной почты и так POST /emailsможет отправлять письма без создания реального ресурса. Btw. Структура URI не несет семантики, поэтому с точки зрения REST не имеет значения, какой тип URI вы используете. Важны метаданные (например, отношение ссылок ), назначенные ссылкам, которые вы отправляли клиентам.

Лучшая идея, которую я до сих пор предлагал, - позволить пользователю API указывать его в качестве дополнения к имени поля (например, / collection? SomeField: gte = someval), чтобы указать, что он должен возвращать ресурсы, где someField больше чем или равно (> =) какому-либо someval. Является ли это хорошей идеей? Плохой идеей? Если да, то почему? Есть ли лучший способ позволить пользователю указать тип сравнения для выполнения с данным полем и значением?

Вам не нужно создавать собственный язык запросов. Я бы предпочел использовать уже существующий и добавить некоторое описание запроса к метаданным ссылки. Возможно, вам следует использовать тип носителя RDF (например, JSON-LD) или использовать пользовательский тип MIME (на самом деле, не существует формата, не поддерживающего RDF, который поддерживает это). Использование существующих стандартов позволяет отделить ваш клиент от сервера, это то, к чему относится унифицированное ограничение интерфейса.

Это было бы эквивалентно / собак? Человек = 123. Есть ли когда-нибудь веская причина для того, чтобы REST URI имел глубину более двух уровней (/ collection / resource_id)?

Как я упоминал ранее, структура URI не имеет значения с точки зрения REST. Вы можете использовать, /x71fd823df2например. Это по-прежнему имеет смысл для клиентов, поскольку они проверяют метаданные, назначенные ссылкам, а не структуру URI. Основное назначение URI - идентификация ресурсов. В стандарте URI утверждается, что путь содержит иерархические данные, а запрос содержит неиерархические данные. Но это может быть очень субъективным, что является иерархическим. Вот почему вы встречаете многоуровневые URI и URI с длинными запросами.

Я много часов искал, но нигде не нашел ответов на свои вопросы (может быть, я просто не знаю, что искать?).

Вы должны прочитать хотя бы ограничения REST из диссертации Fielding , стандарта HTTP и, возможно, веб-API третьего поколения от Markus.

inf3rno
источник