Які найкращі практики щодо вбудованих ресурсів REST?


301

Наскільки я можу сказати, кожен окремий ресурс повинен мати лише один канонічний шлях. Отже, у наступному прикладі, які б гарні шаблони URL-адрес були?

Візьмемо для прикладу представлення компаній про відпочинок. У цьому гіпотетичному прикладі кожна компанія має 0 або більше відділів, і кожен відділ має 0 і більше працівників.

Департамент не може існувати без асоційованої компанії.

Працівник не може існувати без асоційованого відділу.

Тепер я знайшов би природне представлення моделей ресурсів.

  • /companies Колекція компаній - Accepts розміщено для нової компанії. Отримайте всю колекцію.
  • /companies/{companyId}Індивідуальна компанія. Приймає GET, PUT та DELETE
  • /companies/{companyId}/departmentsПриймає POST для нового елемента. (Створює відділ у компанії.)
  • /companies/{companyId}/departments/{departmentId}/
  • /companies/{companyId}/departments/{departmentId}/employees
  • /companies/{companyId}/departments/{departmentId}/employees/{empId}

Враховуючи обмеження, у кожному з розділів я вважаю, що це має сенс, якщо трохи глибоко вкладений.

Однак моя складність виникає, якщо я хочу перелічити ( GET) всіх працівників у всіх компаніях.

Структура ресурсів для цього найтісніше відображатиметься /employees(Колекція всіх працівників)

Чи означає це, що я повинен /employees/{empId}також мати те, що якщо так, то є два URI, щоб отримати один і той же ресурс?

Або, можливо, всю схему слід вирівняти, але це означатиме, що співробітники є вкладеним об'єктом верхнього рівня.

На базовому рівні /employees/?company={companyId}&department={deptId}повертається точно такий же погляд на працівників як найбільш глибоко вкладений зразок.

Яка найкраща практика щодо шаблонів URL-адрес, де ресурси належать іншим ресурсам, але їх можна запитувати окремо?


1
Це майже точно проблема, яка описана в stackoverflow.com/questions/7104578/…, хоча відповіді можуть бути пов'язані. Обидва питання стосуються права власності, але з цього прикладу випливає, що об'єкт верхнього рівня не є власником.
Уес

1
Саме те, що мені було цікаво. Для даного випадку використання ваше рішення здається прекрасним, але що робити, якщо відношення - це агрегація, а не композиція? Досі намагаються розібратися, яка найкраща практика є тут ... Також, чи означає це рішення лише створення відносин, наприклад, наявна особа працевлаштована чи створює предмет для людини?
Якоб О.

Це створює людину на моєму вигаданому прикладі. Причина, що я використовував ці доменні терміни, - це її цілком зрозумілий приклад, хоча і імітує мою актуальну проблему. Ви переглянули пов'язане питання, яке може зупинити вас більше на загостренні відносини.
Уес

Я розділив своє запитання на відповідь і запитання.
Уес

Відповіді:


152

Те, що ви зробили, правильно. Загалом на одному ресурсі може бути багато URI - немає правил, які б говорили, що цього робити не слід.

І взагалі, вам може знадобитися доступ до елементів безпосередньо або як підмножина чогось іншого - тому ваша структура має для мене сенс.

Просто тому, що співробітники доступні під відділом:

company/{companyid}/department/{departmentid}/employees

Не означає, що вони також не можуть бути доступні в компанії:

company/{companyid}/employees

Що б повернуло працівників цієї компанії. Це залежить від того, що потрібно вашому споживачеві клієнту - саме для цього слід розробити.

Але я би сподівався, що всі обробники URL-адрес використовують один і той же код резервного копіювання, щоб задовольнити запити, щоб не дублювати код.


11
Це вказує на дух RESTful, немає правил, які говорять, що ви повинні чи не повинні робити, якщо тільки ви спочатку вважаєте важливим ресурсом . Але далі мені цікаво, яка найкраща практика не дублювати код у таких сценаріях.
abookyun

13
@abookyun, якщо вам потрібні обидва маршрути, то повторний код контролера між ними можна абстрагувати на сервісні об’єкти.
bgcode

Це не має нічого спільного з REST. REST не переймається тим, як ви структуруєте частину вашої URL-адреси шляху ... все, про що вона піклується, є дійсними, сподіваємось, довговічними URI-
адресами

Виконуючи цю відповідь, я думаю, що будь-який api, де динамічні сегменти - всі унікальні ідентифікатори, не повинен обробляти декілька динамічних сегментів ( /company/3/department/2/employees/1). Якщо api надає способи отримання кожного ресурсу, то подача кожного з цих запитів може бути виконана або в бібліотеці на стороні клієнта, або як разова кінцева точка, яка повторно використовує код.
макс

1
Хоча заборони немає, я вважаю більш елегантним мати лише один шлях до ресурсу - робить простіші ментальні моделі простішими. Я також вважаю за краще, щоб URI не змінювали тип свого ресурсу, якщо є які-небудь вкладення. наприклад, /company/*слід повернути лише ресурс компанії та зовсім не змінювати тип ресурсу. REST нічого з цього не визначає - загалом це погано вказано - лише особисті переваги.
kashif

174

Я спробував обидві стратегії проектування - вкладені та не вкладені кінцеві точки. Я виявив, що:

  1. якщо вкладений ресурс має первинний ключ, а у вас немає його основного основного ключа, вкладена структура вимагає отримати його, навіть якщо система насправді цього не вимагає.

  2. вкладені кінцеві точки, як правило, вимагають надлишкових кінцевих точок. Іншими словами, вам найчастіше потрібні додаткові / службові кінцеві точки, щоб ви могли отримати список працівників у різних відділах. Якщо у вас є / працівники, що саме купують / компанії / відділи / працівники?

  3. Кінцеві точки вкладання не розвиваються так добре. Наприклад, вам може не потрібно шукати співробітників зараз, але ви можете пізніше, і якщо у вас вкладена структура, у вас немає іншого вибору, як додати іншу кінцеву точку. За допомогою вкладеного дизайну ви просто додаєте більше параметрів, що простіше.

  4. іноді ресурс може мати декілька типів батьків. У результаті виходить кілька кінцевих точок, всі повертають один і той же ресурс.

  5. надлишкові кінцеві точки ускладнюють писати документи, а також ускладнюють навчання api.

Коротше кажучи, конструкція, що не вкладається, дозволяє більш гнучку та просту схему кінцевої точки.


24
Було дуже освіжаючо зустріти цю відповідь. Я вже кілька місяців використовую вкладені кінцеві точки після того, як навчали, що це був "правильний шлях". Я прийшов до всіх тих же висновків, які ви перераховували вище. Так набагато простіше з вкладеним дизайном.
користувач3344977

6
Ви, здається, перераховуєте деякі недоліки як переваги. "Просто набийте більше параметрів в одну кінцеву точку" ускладнює API документування та навчання, а не навпаки. ;-)
Дренмі

4
Не любитель цієї відповіді. Не потрібно вводити зайві кінцеві точки лише тому, що ви додали вкладений ресурс. Також не проблема, коли один і той же ресурс повертається декількома батьками за умови справжнього володіння вкладеним ресурсом. Не проблема отримати батьківський ресурс, щоб навчитися взаємодіяти з вкладеними ресурсами. Хороший відкритий API REST повинен це зробити.
Скотм

3
@Scottm - Один недолік вкладених ресурсів, який я натрапив, - це те, що він може призвести до повернення невірних даних, якщо ідентифікатори батьківського ресурсу невірні / невідповідні. Якщо припустити, що немає проблем з авторизацією, це залишається до реалізації api, щоб переконатися, що вкладений ресурс насправді є дочірнім батьківським ресурсом, який передається. Якщо ця перевірка не буде зашифрована, відповідь api може бути неправильною, що призведе до корупції. Які ваші думки?
Енді Дуфресне

1
Проміжні батьківські ідентифікатори вам не потрібні, якщо всі кінцеві ресурси мають унікальні ідентифікатори. Наприклад, щоб отримати співробітника за ідентифікатором, ви маєте GET / компанії / відділи / службовці / {empId} або щоб отримати всіх співробітників компанії 123, у вас є GET / компанії / 123 / відділи / службовці / Якщо збереження ієрархічного шляху робить більш очевидним, як ви можете дістатись до проміжних ресурсів для фільтрування / створення / модифікації та допомагає у відкритті на мою думку.
PaulG

77

Я перемістив те, що я зробив, із запитання до відповіді, де більше людей, ймовірно, це бачать.

Що я зробив, це мати кінцеві точки створення у вкладеній кінцевій точці. Канонічна кінцева точка для зміни або запиту елемента не знаходиться в вкладеному ресурсі .

Отже, у цьому прикладі (лише перелік кінцевих точок, що змінюють ресурс)

  • POST /companies/ створює нову компанію повертає посилання на створену компанію.
  • POST /companies/{companyId}/departments коли відділ поміщений, новий відділ повертає посилання на /departments/{departmentId}
  • PUT /departments/{departmentId} модифікує відділ
  • POST /departments/{deparmentId}/employees створює нового працівника, повертає посилання на /employees/{employeeId}

Тож існують ресурси кореневого рівня для кожної колекції. Однак створення знаходиться у об'єкті, що володіє .


4
Я також придумав однотипний дизайн. Я думаю, що інтуїтивно створювати такі речі "там, де вони належать", але потім все-таки мати змогу перелічити їх у глобальному масштабі. Тим більше, коли є стосунки, де ресурс ОБОВ'ЯЗКОВО мати батьків. Тоді створення цього ресурсу в усьому світі не робить цього очевидним, але робити його на такому субресурсі має ідеальний сенс.
Йоаким

Я здогадуюсь, ти використовував POSTзначення PUT, інакше.
Джерардо Ліма

Насправді ні Зауважте, що я не використовую попередньо призначені ідентифікатори для створення, оскільки сервер у цьому випадку несе відповідальність за повернення ідентифікатора (за посиланням). Тому написання POST є правильним (не можна потрапити на ту саму реалізацію). Однак програма змінює весь ресурс, але його все ще доступно в тому самому місці, тому я ПУТЕ його. PUT vs POST - інша справа і теж суперечлива. Наприклад, stackoverflow.com/questions/630453/put-vs-post-in-rest
Уес

@ Навіть я вважаю за краще змінювати методи дієслів, щоб бути під батьківським. Але, чи бачите ви, що параметр передачі запиту для глобального ресурсу прийнятий? Напр .: POST / відділи з параметром запиту company = company-id
Ayyappa

1
@Mohamad Якщо ви вважаєте, що інший спосіб простіший як у розумінні, так і в застосуванні обмежень, сміливо дайте відповідь. Це стосується того, щоб зробити відображення явним у цьому випадку. Він може працювати з параметром, але насправді ось у чому питання. Який найкращий спосіб.
Уес

35

Я прочитав усі вищезазначені відповіді, але, схоже, у них немає спільної стратегії. Я знайшов хорошу статтю про найкращі практики дизайну API в документах Microsoft Documents . Я думаю, вам слід посилатися.

У більш складних системах може бути заманливо надати URI, які дозволяють клієнту переходити через кілька рівнів взаємозв'язків, таких як, /customers/1/orders/99/products.однак, цей рівень складності може бути важким для підтримання і негнучким, якщо відносини між ресурсами змінюватимуться в майбутньому. Натомість намагайтеся зберегти URI порівняно просто . Після того, як у додатку є посилання на ресурс, слід використовувати це посилання, щоб знайти елементи, пов’язані з цим ресурсом. Попередній запит можна замінити на URI, /customers/1/ordersщоб знайти всі замовлення для клієнта 1, а потім /orders/99/productsзнайти продукти в цьому порядку.

.

Порада

Уникайте необхідності використання URI-ресурсів більш складних, ніж collection/item/collection.


3
Посилання, яке ви наводите, вражає разом із точкою, з якої ви виділяєтесь, не створюючи складних URI.
vicco

Тож коли я хочу створити команду для користувача, чи це повинно бути POST / команди (userId в тілі) або POST / users /: id /
groups

@coinhndp Привіт, ви повинні використовувати POST / команди, і ви можете отримати userId після авторизації маркера доступу. Я маю на увазі, коли ви створюєте матеріал, вам потрібен код авторизації, правда? Я не знаю, яку рамку ви використовуєте, але я впевнений, що ви можете отримати userId в контролері API. Наприклад: в API ASP.NET викликайте RequestContext.Principal з методу ApiController. У весняній Secirity вам допоможе SecurityContextHolder.getContext (). GetAuthentication (). GetPrincipal (). У AWS NodeJS Lambda, тобто когніто: ім'я користувача в заголовках.
Довгий Нгуен

Отже, що не так із командами POST / users /: id /. Я думаю, що це рекомендується в документі Microsoft, який ви опублікували вище
coinhndp

@coinhndp Якщо ви створили команду як адміністратор, це добре. Але, як звичайні користувачі, я не знаю, навіщо вам потрібен userId на шляху? Я гадаю, що у нас є user_A та user_B, як ви думаєте, якщо user_A міг би створити нову команду для user_B, якщо user_A викликає POST / users / user_B / команди. Отже, не потрібно передавати userId в цьому випадку, користувач може отримати після авторизації. Але, команди /: id / проекти, наприклад, добре скласти зв’язок між командою та проектом.
Довгий Нгуен

10

Як виглядають ваші URL-адреси, нічого спільного з REST не має. Щось іде. Насправді це "деталь реалізації". Тож так само, як ви називаєте свої змінні. Все, що вони повинні бути, - унікальне та довговічне.

Не витрачайте на це занадто багато часу, просто зробіть вибір і дотримуйтесь його / будьте послідовні. Наприклад, якщо ви йдете з ієрархіями, то ви робите це для всіх своїх ресурсів. Якщо ви перейдете з параметрами запиту ... тощо, так само як імена конвенцій у вашому коді.

Чому так ? Наскільки я знаю, API "RESTful" повинен бути доступним для перегляду (ви знаєте ... "Гіпермедіа як двигун стану програми"), тому клієнт API не переймається тим, якими виглядають ваші URL-адреси, поки вони дійсно (немає SEO, не існує людини, якій потрібно читати ці "дружні URL-адреси", за винятком випадків налагодження ...)

Наскільки приємна / зрозуміла URL-адреса в API REST цікаво лише вам як розробнику API, а не клієнту API, як і назва змінної у вашому коді.

Найголовніше - ваш клієнт API знає, як інтерпретувати ваш тип медіа. Наприклад, він знає, що:

  • у вашому типі медіа є властивість посилань, яка містить список доступних / пов’язаних посилань.
  • Кожне посилання ідентифікується відношенням (подібно до того, як браузери знають, що посилання [rel = "таблиця стилів"] означає його таблицю стилів, або rel = favico - посилання на фавікон ...)
  • і він знає, що означають ці відносини ("компанії" означають список компаній, "пошук" означає шаблонний URL для пошуку за списком ресурсів, "відділи" означають відділи поточного ресурсу)

Нижче наводиться приклад обміну HTTP (тіла знаходяться в ямлі, оскільки простіше писати):

Запит

GET / HTTP/1.1
Host: api.acme.io
Accept: text/yaml, text/acme-mediatype+yaml

Відповідь: список посилань на основний ресурс (компанії, люди, що завгодно ...)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:04:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: this is your API's entrypoint (like a homepage)  
links:
  # could be some random path https://api.acme.local/modskmklmkdsml
  # the only thing the API client cares about is the key (or rel) "companies"
  companies: https://api.acme.local/companies
  people: https://api.acme.local/people

Запит: посилання на компанії (використовуючи body.links.companies попередньої відповіді)

GET /companies HTTP/1.1
Host: api.acme.local
Accept: text/yaml, text/acme-mediatype+yaml

Відповідь: частковий перелік компаній (під позиціями), ресурс містить відповідні посилання, наприклад, посилання для отримання наступної пари компаній (body.links.next) іншого (шаблонного) посилання на пошук (body.links.search)

HTTP/1.1 200 OK
Date: Tue, 05 Apr 2016 15:06:00 GMT
Last-Modified: Tue, 05 Apr 2016 00:00:00 GMT
Content-Type: text/acme-mediatype+yaml

# body: representation of a list of companies
links:
  # link to the next page
  next: https://api.acme.local/companies?page=2
  # templated link for search
  search: https://api.acme.local/companies?query={query} 
# you could provide available actions related to this resource
actions:
  add:
    href: https://api.acme.local/companies
    method: POST
items:
  - name: company1
    links:
      self: https://api.acme.local/companies/8er13eo
      # and here is the link to departments
      # again the client only cares about the key department
      department: https://api.acme.local/companies/8er13eo/departments
  - name: company2
    links:
      self: https://api.acme.local/companies/9r13d4l
      # or could be in some other location ! 
      department: https://api2.acme.local/departments?company=8er13eo

Отже, як ви бачите, якщо ви переходите до посилань / відносин так, як структура структури шляху ваших URL-адрес не має ніякого значення для вашого клієнта API. І якщо ви повідомляєте структуру своїх URL-адрес своєму клієнту як документацію, то ви не робите REST (або, принаймні, не рівень 3 відповідно до " зрілості моделі Річардсона ")


7
"Наскільки приємна / зрозуміла URL-адреса в API REST цікаво лише вам як розробнику API, а не клієнту API, як це було б ім'ям змінної у вашому коді." Чому це НЕ буде цікавим? Це дуже важливо, якщо хтось, крім вас, також використовує API. Це частина досвіду користувачів, тому я б сказав, що дуже важливо, щоб це було зрозуміло для розробників клієнтів API. Зробити речі ще простішими для розуміння, чітко пов'язуючи ресурси, це, звичайно, бонус (рівень 3 у вказаній вами URL-адресі). Все має бути інтуїтивно зрозумілим та логічним із чіткими відносинами.
Йоаким

1
@Joakim Якщо ви створюєте API відпочинку 3 рівня (Hypertext As The Engine Of Application Application), то структура контуру URL-адреси для клієнта абсолютно не представляє інтересу (поки це дійсно). Якщо ви не прагнете до рівня 3, то так, це важливо і має бути доцільним. Але справжній REST - рівень 3. Гарна стаття: martinfowler.com/articles/richardsonMaturanceModel.html
redben

4
Я заперечую проти того, щоб коли-небудь створити API чи інтерфейс користувача, який не є зручним для людей. Рівень 3 чи ні, я погоджуюся, що зв’язування ресурсів - чудова ідея. Але пропонувати це "дає можливість змінити схему URL-адрес" - це бути не на зв’язку з реальністю та тим, як люди використовують API. Тож це погана рекомендація. Але впевнений, що в кращих з усіх світів кожен був би на рівні 3 REST. Я включаю гіперпосилання І використовую зрозумілу для людини схему URL-адрес. Рівень 3 не виключає колишнього, і один ДОЛЖЕН би дбати на мою думку. Хороша стаття, хоча :)
Йоакім

Слід, звичайно, дбати заради ремонтопридатності та інших проблем, я вважаю, що ви пропускаєте точку моєї відповіді: те, як виглядає URL-адреса, не заслуговує на багато роздумів, і вам слід «просто зробити вибір і дотримуватися його / бути послідовний », як я сказала у відповіді. А що стосується API REST, щонайменше, на мою думку, зручність користувачів не в URL-адресі, це здебільшого в (тип медіа) У будь-якому разі, я сподіваюся, ви зрозуміли мою думку :)
redben

9

Я не згоден з таким видом шляху

GET /companies/{companyId}/departments

Якщо ви хочете отримати відділи, я думаю, що краще використовувати ресурс / відділи

GET /departments?companyId=123

Я припускаю, що у вас є companiesтаблиця та departmentsтаблиця, а потім класи для їх відображення в мові програмування, яку ви використовуєте. Я також припускаю, що відділи можуть бути приєднані до інших організацій, ніж компанії, тому ресурс / відділів є простим, зручно мати ресурси, відображені в таблиці, а також вам не потрібно стільки кінцевих точок, оскільки ви можете повторно використовувати

GET /departments?companyId=123

наприклад, для будь-якого виду пошуку

GET /departments?name=xxx
GET /departments?companyId=123&name=xxx
etc.

Якщо ви хочете створити відділ,

POST /departments

слід використовувати ресурс, а орган запиту повинен містити ідентифікатор компанії (якщо відділ може бути пов'язаний лише з однією компанією).


1
Для мене це прийнятний підхід, лише якщо вкладений об'єкт має сенс як атомний об'єкт. Якщо їх немає, насправді не було б сенсу розбивати їх на частини.
Сімме

Це те, що я сказав, якщо ви також хочете мати змогу отримати відділи, тобто якщо ви будете використовувати кінцеву точку / / departments.
Maxime Laval

2
Можливо, також має сенс дозволити включення департаментів шляхом ледачого завантаження під час отримання компанії, наприклад GET /companies/{companyId}?include=departments, оскільки це дозволяє і компанії, і її відділам отримувати один запит HTTP. Фрактал робить це дуже добре.
Меттью Далі

1
Під час налаштування acls ви, мабуть, хочете обмежити доступ до /departmentsкінцевої точки лише адміністратором, а кожна компанія має доступ до власних підрозділів лише через `/ companies / {companyId} / departments`
Cuzox

@MatthewDaly OData також робить це чудово з $ розширенням
Роб Грант

1

Рейки - це рішення для цього: дрібне гніздування .

Я думаю, що це добре, тому що, коли ви маєте справу безпосередньо з відомим ресурсом, немає необхідності використовувати вкладені маршрути, як це обговорювалося в інших відповідях тут.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.