JSON API – работаем по спецификации

    В последнее время веб-разработка разделилась. Теперь мы все не full-stack программисты — мы фронтендеры и бэкендеры. А самое сложное в этом, как и везде, это проблема взаимодействия и интеграции.

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

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



    Начнем издалека — с проблемы, которую мы решаем.

    Давным-давно, в 1959 году Сирил Паркинсон (не путать с болезнью, это писатель и экономический деятель) придумал несколько интересных законов. Например, что расходы растут вместе с доходами и т.д. Один из них называется Законом тривиальности:

    Время, потраченное на обсуждение пункта, обратно пропорционально рассматриваемой сумме.

    Паркинсон был экономистом, поэтому он объяснял свои законы в экономических терминах, примерно так. Если вы придете в совет директоров и скажете, что вам нужны 10 млн долларов на строительство атомной электростанции, скорее всего, этот вопрос будет обсуждаться гораздо меньше, чем выделение 100 фунтов на велосипедный сарай для сотрудников. Потому что все знают, как строить велосипедный сарай, у всех есть свое мнение, все чувствуют себя важными и хотят поучаствовать, а атомная электростанция — это что-то абстрактное и далекое, 10 млн тоже никто никогда не видел — вопросов меньше.

    В 1999 году закон тривиальности Паркинсона появился в программировании, которое тогда активно развивалось. В программировании этот закон встречался в основном в англоязычной литературе и звучал, как метафора. Назывался он The Bikeshed effect (эффект велосипедного сарая), но суть та же самая — велосипедный сарай мы готовы и хотим обсуждать гораздо дольше, чем строительство электростанции.

    В программирование этот термин ввел датский разработчик Poul-Henning Kamp, который участвовал в создании FreeBSD. В процессе проектирования команда очень долго обсуждала то, как должна работать функция sleep. Это цитата из письма Poul-Henning Kamp (разработка тогда велась в e-mail переписке):

    It was a proposal to make sleep(1) DTRT If given a non-integer argument that set this particular grass-fire off I’m not going to say any more about it than that, because it is a much smaller item than one would expect from the length of the thread, and it has already received far more attention than some of the *problems* we have around here.

    В этом письме он говорит, что есть куча гораздо более важных нерешенных задач: «Давайте не будем заниматься велосипедным сараем, уже что-нибудь с этим сделаем и пойдем дальше!»

    Так Poul-Henning Kamp в 1999 году в англоязычную литературу ввел термин bikeshed effect который, можно перефразировать как:

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

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

    Как вы думаете, что важнее: то, как мы общаемся между бэкендом и фронтендом, или бизнес задачи, которые мы делаем? Все считают по-разному, но любой заказчик, человек, который ждет, что вы принесете ему деньги, скажет: «Сделайте мне уже наши бизнес-задачи!» Ему абсолютно все равно, как вы будете передавать данные между бэкендом и фронтендом. Возможно, он даже не знает, что такое бэкенд и фронтенд.

    Подытожить вступление я хотел бы утверждением: API — это велосипедный сарай.


    Ссылка на презентацию доклада

    О спикере: Алексей Авдеев (Avdeev) работает в компании Neuron.Digital, которая занимается нейронками и делает для них классный фронтенд. Также Алексей уделяет внимание OpenSource, и всем советует. Занимается разработкой давно — с 2002 года, застал древний интернет, когда компьютеры были большими, интернет маленьким, а отсутствие JS никого не смущало и все верстали сайты на таблицах.

    Как бороться с велосипедными сараями?


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

    1. Не слушать советы. Я думаю, так себе идея — если не слушать советы, можно такого наворотить, особенно в программировании, и особенно если вы начинающий разработчик.
    2. Делать так, как хотите. «Я художник, я так вижу!» — никакого bikeshed эффекта, делается все, что нужно, но на выходе появляются очень странные вещи. Это часто встречается во фрилансе. Наверняка вы сталкивались с задачами, которые приходилось доделывать за другими разработчиком и реализация которых вызывала у вас недоумение.
    3. Спросить себя важно ли это? Если нет, можно просто не обсуждать, но это вопрос личного сознания.
    4. Использовать объективные критерии. Про этот пункт я как раз буду говорить в докладе. Чтобы избежать эффекта велосипедного сарая, можно использовать критерии, которые объективно скажут, что лучше. Они существуют.
    5. Не говорить о том, о чём не хочешь слушать советы. В нашей компании начинающие бэкенд-разработчики — интроверты, поэтому бывает, что они делают что-то, о чем не рассказывают остальным. В результате мы встречаем сюрпризы. Этот метод работает, но в программировании это не лучший вариант.
    6. Если вас не волнует проблема, ее можно просто отпустить или выбрать любой из предлагаемых вариантов, которые возникли в процессе холиваров.

    Anti-bikeshedding tool


    Я хочу рассказать про объективные инструменты для решения проблемы велосипедного сарая. Чтобы продемонстрировать, что такое anti-bikeshedding tool, расскажу небольшую историю.

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

    Рой Филдинг, автор REST

    На фото Рой Филдинг, который в 2000 году защитил диссертацию «Архитектурные стили и дизайн сетевых программных архитектур» и тем самым ввел термин REST. Более того, он придумал HTTP и, по сути, является одним из основателей Интернета.

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

    Требования к архитектуре REST


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

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

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

       3. Единообразие интерфейса.
    Это один из ключевых базовых принципов, по которым должны строиться REST API. Он включает в себя следующее:

    • Идентификация ресурсов — то, как мы должны строить URL. По REST мы обращаемся к серверу за каким-то ресурсом.
    • Манипуляция ресурсами через представление. Сервер возвращает нам представление, которое отличается от того, что лежит в базе данных. Неважно, храните вы информацию в MySQL или PostgreSQL — у нас есть представление.
    • «Самоописываемые» сообщения — то есть в сообщении лежит id, ссылки, откуда можно еще раз это сообщение получить — все, что нужно, чтобы еще раз работать с этим ресурсом.
    • Гипермедиа — это ссылки на следующие действия с ресурсом. Мне кажется, ни один REST API ее не делает, но она описана Роем Филдингом.

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

    RESTful-блог


    Вернемся к начинающему бэкенд-разработчику, которого попросили сделать сервис для блога на RESTful. Ниже пример прототипа.



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

    Со всеми данными блога мы работаем по принципу СRUD.

    Должна быть возможность любой ресурс создавать, читать, обновлять и удалять. Попробуем попросить нашего бэкенд-разработчика построить RESTful AP Iпо принципу СRUD. То есть написать методы, чтобы создавать статьи, получать список статей или отдельную статью, обновлять и удалять.

    Посмотрим, как он мог бы это сделать.


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


    Это вариант по REST. По принципам идентификации ресурсов мы работаем с ресурсами — со статьями (articles) и пользуемся HTTP-методами, которые предложил Рой Филдинг. Он не мог не использовать свою предыдущую работу в своей следующей работе.

    Для обновления статей многие используют метод PUT, у него немножко другая семантика. Метод PATCH обновляет те поля, которые были переданы, а PUT просто заменяет одну статью на другую. По семантике PATCH — это merge, а PUT — это replace.

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

    Почему так правильно?

    • потому что так сказал Рой Филдинг;
    • потому что это REST;
    • потому что это архитектурные принципы, на которых строится наша профессия сейчас.

    Однако это «велосипедный сарай», будет работать и предыдущий способ. Компьютеры общались до REST, и все работало. Но сейчас в индустрии появился стандарт.

    Удаляем статью


    Рассмотрим пример с удалением статьи. Допустим, есть нормальный, ресурсный метод DELETE /articles, который удаляет статью по id. HTTP содержит заголовки. Заголовок Accept принимает тип данных, которые клиент хочет получить в ответ. Наш джуниор написал сервер, который возвращает 200 OK, Content-Type: application/json, и передает пустой body:

    01.  DELETE /articles/1 НТТР/1.1
    02.  Accept: application/json


    01.  HTTP/1.1 200 OK
    02.  Content-Type: application/json
    03.  null

    Здесь допущена очень частая ошибка — пустой body. Вроде бы все логично — статья удалена, 200 ОК, присутствует заголовок application/json, но клиент, скорее всего, упадет. Он выкинет ошибку, потому что пустой body не валиден. Если вы когда-либо пробовали парсить пустую строку, то сталкивались с тем, что любой парсер json на этом спотыкается и падает.

    Как можно исправить эту ситуацию? Самый, наверное, лучший вариант — это передать json. Если мы заявили: «Accept, отдай нам json», сервер говорит: «Content-Type, я вам отдаю json», отдайте json. Пустой объект, пустой массив — что-то туда положите — это будет решение, и оно будет работать.

    Есть еще решение. Помимо 200 OK есть код ответа 204 — no content. С ним можно не передавать тело. Про это не все знают.

    Так я подвёл к медиатипам.

    MIME-типы


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

    • По умолчанию это text/plain — просто текст.
    • Если ничего не указано, то браузер, скорее всего, будет иметь в виду application/octet-stream — просто поток бит.

    Можно указать просто конкретный тип:

    • application/pdf;
    • image/png;
    • application/json;
    • application/xml;
    • application/vnd.ms-excel.

    Заголовки Content-Type и Accept есть и важны.

    API и клиент должны передавать заголовки Content-Type и Accept.

    Если у вас API построен на JSON, передавайте всегда Accept: application/json и Content-Type application/json.

    Пример типов файлов.


    Медиатипы аналогичны этим типам файлов, только в интернете.

    Коды ответов


    Следующий пример приключений нашего джуниор-разработчика — это коды ответов.



    Самый смешной котд ответа — 200 ОК. Его все любят — он означает, что все прошло правильно. У меня даже был случай — мне приходили ошибки 200 ОК. Реально на сервере что-то упало, в ответ в response приходит HTML-страница, на которой в HTML сверстана ошибка. Я запрашивал application json с кодом 200 ОК, и думал, как же с этим работать? Идешь по response, ищешь слово «ошибка», считаешь, что это ошибка.

    Это работает, однако в HTTP существует много других кодов, которые можно использовать, и по REST Рой Филдинг рекомендует их использовать. Например, на создание сущности (статьи) можно ответить:

    • 201 Created — успешный код. Статья создана, в ответ надо вернуть созданную статью.
    • 202 Accepted означает, что запрос был принят, но его результат будет позже. Это долгоиграющие операции. На Accepted можно не возвращать никакого body. То есть если вы Content-Type в ответе не отдаете, то и body тоже может не быть. Или Content-Type text/plane — все, никаких вопросов. Пустая строка — это валидный text/plane.
    • 204 No Content — тело может вообще отсутствовать.
    • 403 Forbidden — вам нельзя создавать эту статью.
    • 404 Not Found — вы залезли куда-то не туда, нет такого пути, например.
    • 409 Conflict — крайний случай, который мало кто использует. Он бывает нужен, если вы на клиенте, а не на бэкенде генерируете id, а в это время кто-то уже успел создать эту статью. Конфликт — это правильный ответ в таком случае.

    Создание сущности


    Следующий пример: мы создаем сущность, говорим Content-Type: application/json, и передаем этот application/json. Это делает клиент — наш фронтенд. Допустим, создаем эту самую статью:

    01.  POST /articles НТТР/1.1
    02.  Content-Type: application/json
    03.  { "id": 1, "title": "Про JSON API"}


    В ответ может прийти код:

    • 422 Unprocessable Entity — необрабатываемая сущность. Вроде бы все здорово — семантика, есть код;
    • 403 Forbidden;
    • 500 Internal Server Error.

    Но абсолютно непонятно, что конкретно произошло: какая сущность необрабатываемая, почему мне туда нельзя, и что в конце концов случилось с сервером?

    Возвращайте ошибки


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

    Бэкенд может в ответ вернуть массив с ошибками, их может быть несколько.

    01.  HTTP/1.1 422 Unprocessable Entity
    02.  Content-Type: application/json
    03. 
    04.  { "errors": [{
    05.    "status": "422",
    06.    "title": "Title already exist",
    07.  }]}


    У каждой ошибки может быть свой статус и заголовок. Это здорово, но это уже идет на уровне соглашений поверх REST. Это может быть нашим anti-bikeshedding инструментом, чтобы перестать спорить, а делать сразу хороший правильный API.

    Добавим паджинацию


    Следующий пример: к нашему начинающему бэкенд-разработчику приходят дизайнеры и говорят: «У нас много статей, нам нужна паджинация. Мы нарисовали вот такую».


    Рассмотрим ее подробней. Прежде всего в глаза бросается 336 страниц. Когда я это увидел, то подумал, как эту цифру вообще получить. Откуда взять 336, ведь на запрос списка статей мне приходит список статей. Например, их там 10 тысяч, то есть мне надо загрузить все статьи, поделить на количество страниц и узнать это число. Очень долго я буду грузить эти статьи, нужен способ получить количество записей быстро. Но если наш API отдает список, то куда это количество записей вообще засунуть, потому что в ответ приходит массив статей. Получается, раз количество записей нигде не ставится, то его надо в каждую статью добавлять, чтобы каждая статья говорила: «А нас всех столько-то!».

    Однако есть соглашение поверх REST API, которое решает эту проблему.

    Запрос списка


    Чтобы API был расширяемый, можно сразу использовать GET-параметры для паджинации: размер текущей страницы и её номер, чтобы нам вернулся ровно тот кусок той страницы, который мы запросили. Это удобно. В ответ можно не сразу давать массив, а добавить дополнительную вложенность. Например, ключ data будет содержать массив, данные, которые мы запросили, а ключ meta, которого до этого не было, будет содержать общее количество.

    01.  GET /articles?page[size]=30&page[number]=2
    02.  Content-Type: application/json

    01.  HTTP/1.1 200 OK
    02.  {
    03.    "data": [{ "id": 1, "title": "JSONAPI"}, ...],
    04.    "meta": { "count": 10080 }
    05.  }

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

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

    [offset]...[limit]


    01.  GET /articles?page[offset]=30&page[limit]=30
    02.  Content-Type: application/json

    01.  HTTP/1.1 200 OK
    02.  {
    03.    "data": [{ "id": 1, "title": "JSONAPI"}, ...],
    04.    "meta": { "count": 10080 }
    05.  }

    У тех, кто работает с базами данных, возможно, уже на подкорке [offset]...[limit]. Использовать его вместо page[size]...page[number] будет проще. Это немножко другой подход.

    Курсорная паджинация


    01. GET /articles?page[published_at]=1538332156
    02. Content-Type: application/json


    01. HTTP/1.1 200 OK
    02. {
    03.     "data": [{ "id": 1, "title": "JSONAPI"}, ...],
    04.     "meta": { "count": 10080 }
    05. }


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

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

    Проблема N +1


    Следующая проблема, с которой обязательно столкнется наш джуниор-разработчик — это проблема N + 1 (бэкендеры поймут). Допустим, нужно вывести список из 10 статей. Мы загружаем список статей, у каждой статьи есть автор, и для каждой нужно загрузить автора. Мы отправляем:

    • 1 запрос на получение списка статей;
    • 10 запросов для получения авторов каждой статьи.

    Итого: 11 запросов, чтобы вывести небольшой список.

    Добавляем связи


    На бэкенде эта проблема решена во всех ORM — надо только не забывать дописывать эту связь. Эти связи можно использовать и на фронтенде. Делается это следующим образом:

    01.  GET /articles?include =author
    02.  Content-Type: application/json

    Можно использовать специальный GET-параметр, назвать его include (как на бэкенде), говоря, какие связи нам нужно загрузить вместе со статьями. Допустим, мы загружаем статьи, и хотим вместе со статьями сразу же получить еще их автора. Ответ выглядит так:

    01. НТТР/1.1 200 ОК
    02. { "data": [{
    03.   { attributes: { "id": 1, "title": "JSON API" },
    04.   { relationships: {
    05.    "author": { "id": 1, "name": "Avdeev" } }
    06.   }, ...
    07. }]}


    В data перенесены собственные атрибуты статей и добавлен ключ relationships (связи). В этот ключ мы кладем все связи. Таким образом одним запросом мы получили все те данные, которые до этого получали 11 запросами. Это крутой лайфхак, который хорошо решает проблему с N + 1 на фронтенде.

    Проблема дублирования данных


    Допустим, нужно вывести 10 статей с указанием автора, у всех статей один автор, но объект с автором очень большой (например, очень длинная фамилия, которая занимает мегабайт). Один автор включен в ответ 10 раз, и 10 включений одного и того же автора в ответ займет 10 Мбайт.

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

    01. НТТР/1.1 200 ОК
    02. { "data": [{
    03.  "id": "1″, "type": "article",
    04.  "attributes": { "title": "JSON API" },
    05.  "relationships": { ... }
    06.   "author": { "id": 1, "type": "people" } }
    07. }, ... ]
    08. }


    Мы помечаем все сущности каким-то типом (это тип репрезентации, тип ресурса). Рой Филдинг ввел понятие ресурса, то есть запросили статьи — получили «article». В relationships мы помещаем ссылку на тип people, то есть у нас еще где-то лежит ресурс people. А сам ресурс мы берем в отдельный ключ included, который лежит на одном уровне с data.

    01. НТТР/1.1 200 ОК
    02. {
    03. "data": [ ... ],
    04.  "included": [{
    05.   "id": 1, "type": "people",
    06.   "attributes": { "name": "Avdeev" }
    07. }]
    08. }


    Таким образом, все связанные сущности в единственном экземпляре попадают в специальный ключ included. Мы храним только ссылки, а сами сущности хранятся в included.

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

    Нужны не все поля ресурса


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

    GET /articles?fields[article]=title НТТР/1.1

    01. НТТР/1.1 200 OK
    02. { "data": [{
    03.  "id": "1″, "type": "article",
    04.  "attributes": { "title": "Про JSON API" },
    05.   }, ... ]
    06. }


    Если нужна, например, еще и дата публикации, можно написать через запятую «published date». В ответ в attributes придет два поля. Это соглашение, которое можно использовать как anti-bikeshedding tool.

    Поиск по статьям


    Часто нам нужны поиски и фильтры. Для этого есть соглашения — специальные GET-параметры filters:

    GET /articles?filters[search]=api HTTP/1.1 — поиск;
    GET /articles?fiIters[from_date]=1538332156 HTTP/1.1 — загрузить статьи с определенной даты;
    GET /articles?filters[is_published]=true HTTP/1.1 — загрузить статьи, которые только опубликованы;
    GET /articles?fiIters[author]=1 HTTP/1.1 — загрузить статьи с первым автором.

    Сортировка статей


    GET /articles?sort=title НТТР/1.1 — по заголовку;
    GET /articles?sort=published_at HTTP/1.1 — по дате публикации;
    GET /articles?sort=-published_at HTTP/1.1 — по дате публикации в обратном направлении;
    GET /articles?sort=author,-publisbed_at HTTP/1.1 — сначала по автору, потом по дате публикации в обратном направлении, если статьи у одного автора.

    Нужно поменять URLs


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

    01. GET /articles НТТР/1.1
    02. {
    03.  "data": [{
    04.   ...
    05.   "links": { "self": "http://localhost/articles/1"
    },
    06.   "relationships": { ... }
    07.  }],
    08.  "links": { "self": "http://localhost/articles" }
    09. }

    Или related, если мы хотим подсказать клиенту, как загрузить комментарий к этой статье:

    01. ...
    02. "relationships": {
    03.  "comments": {
    04.   "links": {
    05.   "self": "http://localhost/articles/l/relationships/comments
    ",
    06.   "related": "http://localhost/articles/l/comments"
    07.   }
    08.  }
    09. }

    Клиент видит, что есть ссылка, переходит по ней, загружает комментарий. Если ссылки нет, значит, комментариев нет. Это удобно, но так мало кто делает. Филдинг придумал принципы REST, но не все из них зашли в нашу индустрию. В основном мы пользуемся двумя-тремя.

    В 2013 году все лайфхаки, о которых я вам рассказал, Steve Klabnik объединил в спецификацию JSON API и зарегистрировал как новый media type поверх JSON. Так наш джуниор бэкенд-разработчик, постепенно эволюционируя, пришел к JSON API.

    JSON API


    На сайте http://jsonapi.org/implementations/ всё подробно описано: есть даже список 170 различных реализаций спецификаций для 32 языков программирования — и это только добавленные в каталог. Уже написаны библиотеки, парсеры, сериализаторы и пр.

    Поскольку эта спецификация опенсорсная, в неё все вкладываются. Я, в том числе, что-то сам написал. Уверен, таких людей много. Вы можете сами присоединиться к этому проекту.

    Плюсы JSON API


    Cпецификация JSON API решает ряд проблем — общее соглашение для всех. Раз есть общее соглашение, то мы не спорим внутри команды — велосипедный сарай задокументирован. У нас есть соглашение, из каких материалов делать велосипедный сарай и как его красить.

    Теперь, когда разработчик делает что-то неправильно и я это вижу, то не начинаю дискуссию, а говорю: «Не по JSON API!» и показываю на место в спецификации. Меня ненавидят в компании, но постепенно привыкают, и JSON API всем начал нравиться. Новые сервисы по умолчанию мы делаем по этой спецификации. У нас есть ключ date, мы готовы добавлять ключи meta, include. Для фильтров есть зарезервированный GET-параметр filters. Мы не спорим, как назвать фильтр — используем эту спецификацию. В ней описано, как делать URL.

    Поскольку мы не спорим, а делаем бизнес задачи, производительность разработки выше. У нас спецификации описаны, бэкенд разработчик прочитал, сделал API, мы его прикрутили — заказчик счастлив.

    Популярные проблемы уже решены, например, с паджинацией. В спецификации много подсказок.

    Поскольку это JSON (спасибо Дугласу Крокфорду за этот формат), он лаконичней XML, его довольно легко читать и понимать.

    То, что это Open Source может быть и плюсом, и минусом, но я люблю Open Source.

    Минусы JSON API


    Объект разросся (date, attributes, included и пр.) — фронтенду надо парсить ответы: уметь перебирать массивы, ходить по объекту и знать, как работает reduce. Не все начинающие разработчики знают эти сложные вещи. Есть библиотеки сериализаторы/десериализаторы, можно пользоваться ими. Вообще это просто работа с данными, но объекты большие.

    А у бэкенда начинается боль:

    • Контроль вложенности — include можно залезть очень далеко;
    • Сложность запросов к БД — они строятся иногда автоматически, и получаются очень тяжелыми;
    • Безопасность — можно залезть в дебри, особенно если подключить какую-то библиотеку;
    • Спецификация сложно читается. Она на английском, и это некоторых отпугнуло, но постепенно все привыкли;
    • Не все библиотеки реализуют спецификацию хорошо — это проблема Open Source.

    Подводные камни JSON API


    Немножко хардкора.

    Количество relationships в выдаче не ограничено. Если мы делаем include, запрашиваем статьи, добавляя к ним комментарии, то в ответ нам придут все комментарии этой статьи. Есть 10 000 комментариев — получи все 10 000 комментариев:

    GET /articles/1?include=comments НТТР/1.1

    01. ...
    02. "relationships": {
    03.  "comments": {
    04.   "data": [0 ... ∞]
    05.  }
    06. }


    Таким образом на наш запрос в ответ пришло реально 5 Мбайт: «В спецификации так и написано — надо правильно переформулировать запрос:

    GET /comments?filters[article]=1&page[size]=30 HTTP/1.1

    01. {
    02. "data": [0 ... 29]
    03. }


    Мы запрашиваем комментарии с фильтром по статье, говорим: «30 штучек, пожалуйста» и получаем 30 комментариев. Это и есть неоднозначность.

    Одни и те же вещи можно неоднозначно сформулировать:

    GET /articles/1?include=comments HTTP/1.1 — запрашиваем статью с комментариями;
    GET /articles/1/comments HTTP/1.1 — запрашиваем комментарии к статье;
    GET /comments?filters[article]=1 HTTP/1.1 — запрашиваем комментарии с фильтром по статье.

    Это одно и то же — одни и те же данные, которые получаются по-разному, возникает некоторая неоднозначность. Этот подводный камень сразу не видно.

    Полиморфные связи «один ко многим» очень быстро вылезают в REST.

    01. GET /comments?include=commentable НТТР/1.1
    02.
    03. ...
    04. "relationships": {
    05.  "commentable": {
    06.   "data": { "type": "article", "id": "1″ }
    07. }
    08. }


    На бэкенде есть полиморфная связь commentable — она вылезает в REST. Так и должно произойти, но ее можно замаскировать. В JSON API не замаскируешь — она вылезет.

    Сложные связи «многие ко многим» с дополнительными параметрами. Тоже все связующие таблицы вылезают:

    01. GET /users?include=users_comments НТТР/1.1
    02.
    03. ...
    04. "relationships": {
    05.  "users_comments": {
    06.   "data": [{ "type": "users_comments", "id": "1″ }, ...]
    07.  },
    08. }


    Swagger


    Swagger — это интерактивный инструмент для написания документации.

    Допустим, нашего бэкенд-разработчика попросили написать документацию к его API, и он ее написал. Это легко, если API простой. Если же это JSON API, Swagger так легко не напишешь.

    Пример: Swagger магазина животных. Каждый метод можно открыть, посмотреть response и примеры.



    Так выглядит пример модели Pet. Здесь классный интерфейс, все просто читается.



    А так выглядит создание модели JSON API:



    Это уже не так здорово. Нам нужно data, в data что-то с relationships, included содержит 5 типов модели и т.д. Swagger можно написать, Open API — мощная вещь, но сложно.

    Альтернатива


    Есть спецификация OData, которая появилась чуть позже — в 2015 году. Это «The best way to REST», как заверяет официальный сайт. Выглядит следующим образом:

    01. GET http://services.odata.org/v4/TripRW/People HTTP/1.1 — GET-запрос;
    02. OData-Version: 4.0 — специальный заголовок с версией;
    03. OData-MaxVersion: 4.0 — второй специальный заголовок с версией

    Ответ выглядит так:

    01. HTTP/1.1 200 OK
    02. Content-Type: application/json; odata.metadata=minimal
    03. OData-Version: 4.0
    04. {
    05.  ’@odata.context’:  ’http://services.odata.org/V4/
    06.  ’@odata.nextLink’  : ’http://services.odata.org/V4/
    07.  ’value’: [{
    08.   ’@odata.etag’: 1W/108D1D5BD423E51581′,
    09.   ’UserName’: ’russellwhyte’,
    10.   ...

    Здесь расширенный application/json и объект.

    Мы не стали использовать OData, во-первых, поскольку это то же самое, что JSON API, но он не лаконичный. Там огромные объекты и мне кажется, что все гораздо хуже читается. OData тоже вышел в Open Source, но он сложнее.

    Что с GraphQL?


    Естественно, когда мы искали новый формат API, мы нарвались и на этот хайп.

    Высокий порог входа.

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

    Эффект большого взрыва.

    Если в проекте не было никакого API, и мы стали использовать GraphQL, через месяц мы поняли, что он нам не подходит, будет поздно. Придется писать костыли. С JSON API или с OData можно эволюционировать — простейший RESTful, прогрессивно улучшаясь, превращается в JSON API.

    Ад на бэкенде.

    GraphQL вызывает ад на бэкенде — прямо один в один, как и полностью реализованный JSON API, потому что GraphQL получает полный контроль над запросами, а это библиотека, и вам нужно будет решать кучу вопросов:

    • контроль вложенности;
    • рекурсия;
    • ограничение частоты;
    • контроль доступа.

    Вместо выводов


    Рекомендую прекратить спорить по поводу велосипедного сарая, а взять anti-bikeshedding tool в качестве спецификации и просто делать API по хорошей спецификации.

    Чтобы найти свой стандарт для решения проблемы велосипедного сарая, может посмотреть эти ссылки:

    http://jsonapi.org
    http://www.odata.org
    https://graphgl.org
    http://xmlrpc.scripting.com
    https://www.jsonrpc.org

    Контакты Спикера Алексея Авдеева: alexey-avdeev.com и профиль на github.

    Коллеги, мы открыли прием докладов на Frontend Conf, которая пройдет 27 и 28 мая в рамках РИТ++. Наш программный комитет начал работу, чтобы за следующие три месяца собрать классную программу.

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

    Следите за ходом подготовки через рассылку, а идеи, кого стоит пригласить, о какой теме поговорить, пишите прямо в комментарии к статье.
    Конференции Олега Бунина (Онтико)
    803,00
    Конференции Олега Бунина
    Поделиться публикацией

    Комментарии 93

      +1
      Обязательно (и об этом джуниоры не знают) в ответ возвращайте ошибки. Это семантично и правильно.

      А ФБ, яндекс, вроде инстаграм и нетфликс об этом не знают и возвращают 200 на всё, наверно глупые.
      Эта тема многократно обсуждалась на хабре. Если бы вы писали, например, мобильные приложения на типизированных языках, то не были бы столь категоричны. Я в своих сервисах тоже везде отвечаю 200 со стандартным телом, где есть поле isSuccess, есть код и есть описание ошибки, в случае ее наличия. Это удобнее для всего, начиная от логирования и заканчивая клиентом.
        +5
        Согласен с автором статьи.
        Если бы вы писали, например, мобильные приложения на типизированных языках, то не были бы столь категоричны. Я в своих сервисах тоже везде отвечаю 200 со стандартным телом, где есть поле isSuccess, есть код и есть описание ошибки, в случае ее наличия.

        Пишу на swift, до этого на obj-c писал. И нет, коды ошибок http крайне важный и помогают на этапе до парсинга определить что произошла ошибка. И я всегда настаиваю при работе с бекенд-коллегами чтобы ошибки отдавались с соответствующим http кодом.
        Например что-то произошло с авторизацией. Слетела она по какой-то причине. Можем послать 401 код, в любом запросе, и это будет означать что авторизация не прошла — надо локально почистить данные и выполнить специфичные для этой ошибки действия (например попросить авторизоваться).
        А когда всё приходит со статусом 200 — то нужно каждый раз разбираться и парсить — ошибка тут пришла (и какая — надо просто показать сообщение или нужны действия) или объект.

        P.S. вообще, то что описано очень похоже на то, как реализован REST модуль в Yii2. Одна из самых удобных, на мой взгляд, реализаций, REST, с которыми мне приходилось работать.
          +11
          Отображать всё множество ошибок приложения на нищебродский список HTTP кодов, а потом обратно, поддерживать этот маппинг, всем действующим лицам запоминать, обновлять спеки, делать два сложных уровня обработки ошибок вместо одного — так себе занятие, для не сильно занятых перфекционистов, прямо скажем.

          Потому здоровый минимум ответов бэка, например:
          200 OK — сервер справился с запросом, что из этого вышло — читайте в приложенном объекте,
          403 Forbidden — низзя,
          404 Not Found — не туда,
          500 Internal Server Error — случилось что-то очень страшное, нет смысла объяснять что именно, действуйте по ситуации.

          Ну и 401 Unauthorized, если хочется на прикрытые эндпоинты достукиваться вот прям из браузера.

          Остальное — в поле .error, по отсутствию которого легко определить удавшийся на 100% запрос.

          Затариваться на проекте всей номенклатурой HTTP кодов, а также PUT, PATCH, DELETE и прочим сатанизмом — прямой путь к знакомству с особенностями работы корпоративных и просто кривых проксей. Упомянутые монстры соцсетизма не просто так эти дела упростили, а чтобы сэкономить на поддержке клиентов у которых «тормозит, не работает, ставлю 1 балл пока».

          Другое личное ИМХО в том, что разрешать в продакшне неконтролируемые запросы от фронта (свободные фильтры с неограниченными параметрами и прочую логику построения запроса к модели, в REST форме или любой другой — дичайший моветон и грабли.

          Берегите лбы, товарищи!
            +1
            Согласен. Совсем упарываться не надо, и описанное в статье неплохо в теории и в идеале, но всегда «реальность вводит коррективы».
            Собственно я такой блок ответов и использую.
            200 — ок
            401/403 — проблема авторизации (+ error поле)
            400/500 — просто ошибка, что-то пошло не так (+ error поле)
              +1
              Я думаю, коды ошибок HTTP следует оставить для ситуаций, когда отвечает не знакомый REST-сервер, а оказавшееся на его месте нечто другое: прокси, свежеустановленный пустой веб-сервер, чужой сайт или REST-сервер, заглушка роскомцензуры. И, соответственно, при обработки этих кодов следует исходить из того, что никакого json'а в теле ответа нет, и, соответственно, не пытаться его искать и парсить.
              А серверу, в свою очередь, следует использовать эти коды, если есть серьёзные основания полагать, что к нему пришёл вообще не REST-клиент, а нечто странное: опечатавшийся человек, гугловый робот и т.п.
                +2
                Я бы к этому списку ещё 400 Bad Request добавил
                  –1
                  Проще:
                  2хх — всё удачно
                  4хх — Вы чет не то прислали
                  5хх — Ой, чет на сервере набаранили

                  3хх — сервисные
                  +2
                  Как вы отличаете нет соли или нет магазина с солью? 404 потому что uri битый или 404 потому что /items/1 не существует?
                  И даже страшно представить сколько у вас бойлерплейта на клиенте для сортировки ошибок.
                    –1
                    Я так не загоняюсь. Нет соли в магазине — значит например 400/404/любой другой с сообщением (полем error).
                    404 битый url — просто мы не получим сообщение от json в body от сервера и выводим ошибку вида «что-то пошло не так, попробуйте позже или сообщить нам о проблеме»
                      0
                      В обоих случаях вам приходит одинаковая 404. Как вы решаете, что показать юзеру? А если в случае «нет соли в магазине» вам приходит пояснение в теле, то как вы определяете надо ли парсить тело?
                        –1
                        Да, тело парсится всегда. Если не найдено ничего в теле — то показывается «неизвестная ошибка». А так — всегда в теле должно быть что-то
                        Да, всегда одинаковая. Если нет тела, то показываем «универсальную» ошибку.
                        Я больше придерживаюсь принципа — если всё ОК — то код 200.
                        Во всех остальных случаях — любой не 2хх код и текст ошибки в теле.
                        А 401 и прочее — это для обработки «особых» случаев и особых ошибок.
                        Ну можно упороться и в тело помимо ошибки определить ещё какой-то протокол, по которому определять действия. Но это уже изврат я считаю.
                          0
                          Я правильно понимаю, что у вас сделано так: если 200, то парсим тело в полезный объект. Если 404, то смотрим в тело, если оно есть, то парсим в объект ошибки, если нет, то показываем ошибку?
                            –1
                            Да
                              0

                              А с моим подходом при любом 200 тело парсится в один и тот же объект. А все остальные ответы сразу отправляются в невалидные, потому что они возможны только при отладке или падении чего-то. Это значительно упрощает клиента.

                                –1
                                Тоже хорошой подход. Осталось заставить бекенд разработчиков поддерживать единообразие. К сожалению, в моей работе вполне возможны варианты, когда формат ответа меняется, выкатывается на бой, а мобильных разработчиков предупредить забыли.
                                + бывают разные типы ошибок.
                                Поэтому я предпочитаю, что если успех — то это success блок, и отрабатывается по одному алгоритму.
                                А если ошибка — то в отдельном месте определяем что за ошибка (от сервера пришла, сетевая ошибка, сервак лёг) и обрабатываю это отдельно.
                                Мне кажется что чёткое разделение — вот здесь точно нет ошибки,
                                а здесь точно ошибка — более гибкое и меньше всяких условий получается.
                                Но это моё ИМХО, основанное именно на моём опыте.
                    0

                    Многие либы обертки для http бросают исключения при не 200, а это противоестественно когда код ветвится за счет try catch, для этого придуман if.

                    0
                    То-то Netflix либу для JSON API поддерживают, github.com/Netflix/fast_jsonapi, чтобы потом двухсоточки возвращать. Впрочем не удивлюсь, если какие-то отделы это делают, возможно в силу каких-то объективных причин, типа телевизоров или приставок, не умеющих в http коды или чтобы вообще не парсить http заголовок, хотя сильно сомневаюсь.

                    Задам встречный вопрос, может фб, Инстаграм и Яндекс просто знают, что многие, кто будет использовать их апи, не читают стандарты и не поймут эти коды или проигнорируют их? Или что пользователи напишут что-то вроде
                    raise if response.code >= 400
                    к примеру. И давать советы забить на http коды в общем-то только поощряют эту практику.

                    Никто в принципе не заставляет пользователей апи использовать http коды, если удобно проверять поле error какое-нибудь. Но возвращать их — хороший подход. И писать клиент, используя их, в общем случае проще и удобнее.

                    от логирования
                    если только логируете вы в всё в один файл и пользователей у вас 1.5 человека. Фильтровать по статусу весьма удобно, когда это необходимо.
                      +1
                      В докладе я говорю о том, что любое решение будет работать. Но вместо того, чтобы спорить по поводу правильных HTTP-кодов и названий полей, вроде isSuccess, лучше взять готовую спеку и начать делать то, что нужно заказчику.
                      +2
                      HTTP был предложен в марте 1991 года Тимом Бернерсом-Ли, а Roy T. Fielding присоединился в 1993… Один из основателей Apache HTTP Server Project.
                        0
                        Спасибо за замечание. Согласен, слово «придумал» не очень корректно. Я имел ввиду, что он является одним из авторов.
                        –1
                        Поясните пожалуйста, зачем указывать тип объекта еще раз в запросе
                        GET /articles?fields[article]=title НТТР/1.1
                          +2

                          Через include можно наподключать смежных сущностей, а через fields задать полей, которых мы будем ждать в смежных сущностях.
                          Например:
                          GET /articles?include=authors&fields[article]=title&fields[author]=id,name

                            –1
                            Ну, все равно получается избыточность. Если для include, то понятно. Но если не указывать тип, можно предположить что поля для того же объекта, что и запрос. Т.е. мне лично такой синтаксис кажется избыточным. Я бы сделал что-то типа:
                            GET /articles?fields=title&include=authors&fields[author]=id,name
                              –1
                              Получается немного, да.
                              Но я думаю, что при составлении спецификации решили, что в данном случае единообразие интерфейса и минимизация ошибок важнее избыточности.
                                –1

                                В вашем примере fields одноременно строка и массив. Сервер просто проигнорирует fields=title в подобном запросе.

                                  0

                                  Это зависит от реализации сервера.
                                  RFC 3986 никак не определяет формат передачи массива через GET-параметры.


                                  В общем случае на сервер придёт строка вся query string.


                                  Если сервер разберёт её по аргументам, то, скорее всего, будет вот так:


                                  {
                                    'fields': 'title',
                                    'include': 'authors',
                                    'fields[author]': 'id,name'
                                  }

                                  Если игнорирование и будет, то только если оно специально так настроено.

                                    0

                                    Вы правы, мое замечание относится только к PHP. Он распарсит эту строку как я написал:


                                    array(2) {
                                      ["include"]=>
                                      string(7) "authors"
                                      ["fields"]=>
                                      array(2) {
                                        ["article"]=>
                                        string(5) "title"
                                        ["author"]=>
                                        string(7) "id,name"
                                      }
                                    }

                                    http://sandbox.onlinephpfunctions.com/code/549e94093e8565c828811eb2506f80d09a2fa390

                                      0
                                      Причём не просто к PHP а к его стандартным способам парсинга. Вы в полном праве игнорировать их наличие и парсить query string и(или) тело запроса как угодно. В частности никто не мешает вам отправлять в query string json.
                            0

                            Ну, наверное, вот для этого:


                            GET /articles?include=author&fields[article]=title,body&fields[person]=name

                            Чтобы стандартный парсер Query String в любом веб-фреймворке преобразовал это в


                            {
                              include: "author",
                              fields: {
                                article: "title,body",
                                person: "name"
                              }
                            }
                              +2
                              Статья вроде рассчитана на начинающих бекендеров, и сразу предлагается в Rest запихивать кучу условий для SQL запросов. Что-то мне подсказывает, что начинающий бекендер наплодит такой «гибостью» кучу дыр для SQL-инъекций…
                                +1
                                Даже в случае использования параметрических запросов? ИМХО конечно, но если %framework% на проекте не совсем древний / дремучий, то на сегодняшний день не использовать параметризацию (кроме очень особых случаев) — это просто какая-то дичь. Code review, опять же, на то и тимлид / сеньоры, чтобы джун не дремал.
                                  0

                                  Тяжело использовать параметрические запросы с переменным числом параметров…

                                    0
                                    Не совсем понял, почему тяжело?
                                      –1
                                      Потому что нужно согласованно собирать сам запрос и список параметров в двух разных переменных, да ещё и назначать всем уникальные идентификаторы — а это почти невозможная задача для FullStackOverflow-девелоперов :-)
                                        0
                                        Не вполне согласен. Во-первых, при использовании современных фреймворков решение вышеозначенной задачи зашито в подсистеме абстракции БД, и работа того джуна оказывается на уровне элементарной профпригодности («посмотри как сделано десятью строками выше, и сделай похожим образом вот тут»). Во-вторых, может быть у нас разные рынки труда, но у меня на работе джуны способны и читать и писать простейшие манипуляции с массивами. Code review и автотесты ИМХО вполне способны закрыть большинство оставшихся проблем.
                                          0
                                          Я тоже так думал, пока не увидел как в унаследованном проекте собирались запросы.
                                  +1
                                  Не предлагается сразу всё запихивать. Большинство условий являются необязательными и просто иллюстрируют «как можно делать, если Вам нужно».
                                  +5
                                  Ну а вообще, приоритет универсальности спецификации API далеко не всегда оправдан.
                                  На самом деле, автор исходит из посылки, что изменение API — это сильно больнее сложности.
                                  На практике, я часто вижу примеры, когда перебарщивают с универсальностью, порождая сложность. А потом оказывается, что эта универсальность не так уж и нужна. Зато сколько времени потрачено на вылизывание всяких гипотетических кейсов от тестеров, когда шлются дикие комбинации параметров, которые ломают логику, и тестеры радостно кричат «Ага!». Когда запрос четко валидируется на на уровне схемы и мапится в статику, это делает API сильно проще и надежнее. А добавить поле в ответ — ну, отрефакторим. Если нормально налажена работа в команде и бэкенд не пребывает в состоянии холодной войны с фронтендом, то никакой драмы. Ну и версионирование никто не отменял.
                                    0
                                    Согласен, валидация (всякими JSON Schema) необходима — если запрос не по схеме, то возвращать что-то типа 400 с описанием этой самой ошибки (что необходимо, что лишнее). Потому что если запрос не проходит валидацию, то даже пытаться его обработать смысла особого нет. Соответственно, и мапинг в статику — тоже очень полезная вещь (конечно, когда эта статика есть), это обнаружит потенциальные проблемы на уровне компиляции (опять же, когда она есть), а не после деплоя (или, еще хуже, на продакшене).
                                    0
                                    Ну к слову про связи, можно развесистые графы свойств именовать и на бекенде регистрировать, а клиенты должны будут передать только имя представления.

                                    1. Ограничивает жадность клиентов.
                                    2. Позволяет извлекать большие сложные графы, описание которых сложно затолкать в URL.
                                      0
                                      Странно что ничего не упомянули про JSON-RPC v2.0
                                      Вполне себе хорошая вещь.
                                        0
                                        Если про RPC говорить, то, наверное, и про gRPC надо вспомнить. Но тут же вроде про REST.
                                          0
                                          Про RPC было в ранней версии доклада, но пришлось убрать, чтобы уложиться в полчаса и донести мысль не очень скомкано. :)

                                          Остались только ссылки в конце презентации на спецификации XML-RPC и JSON-RPC для дальнейшего изучения.
                                          –1
                                          Увидел метод PATCH и вздрогнул — похоже автор забывает о такой сфере утилизации человеческих ресурсов, как «системное администрирование». Скучающий сисадмин запросто может настроить корпоративный прокси на поддержку GET, POST запросов и блокирование остальных видов запросов, как «опасных», что приведет к поломке нашего API, использующего PATCH.

                                          И еще возник вопрос допустимости использования не закодированных символов "[" и "]" в URL.
                                            0
                                            В спеке есть про это, если я правильно понял Ваше замечание.
                                            Some clients, like IE8, lack support for HTTP’s PATCH method. API servers that wish to support these clients are recommended to treat POST requests as PATCH requests if the client includes the X-HTTP-Method-Override: PATCH header. This allows clients that lack PATCH support to have their update requests honored, simply by adding the header.
                                            jsonapi.org/recommendations/#patchless-clients

                                            На вопрос тоже нашёл ответ в спеке:
                                            Note: The above example URI shows unencoded [ and ] characters simply for readability. In practice, these characters must be percent-encoded, per the requirements in RFC 3986.
                                            jsonapi.org/format/#fetching-sparse-fieldsets
                                              +1
                                              Some clients, like IE8...

                                              — нет, не правильно, дело не в клиенте, а в настройках корпоративного proxy-сервера которые режут все запросы клиента, кроме простейших GET/POST. Это касается любых клиентов, Хром, FF-наипоследнийший — не важно. К серверу запрос придет от прокси или не придет. Короче, экзотические типы запросов, это риск потерять часть функциональности для некоторых пользователей. Ваше замечание со ссылкой на спецификацию, тоже актуально, так как редкий сервер или бэкенд фреймворк будет конвертировать «X-HTTP-Method-Override: PATCH» в PATCH (точно не знаю, не проверял, но испытываю сильнейшие сомнения). Как результат на бэкенде мы получим POST, если запрос вообще до нас дойдет.

                                              these characters must be percent-encoded

                                              — это я и имел в виду, из статьи это явно не следует и велика вероятность что фронтэнд разработчик забудет закодировать такие символы
                                            0
                                            Как организовать удаление нескольких объектов одним DELETE-запросом, используя «правильный» REST API? DELETE-запрос не должен содержать тела.
                                              0

                                              http://springbot.github.io/json-api/extensions/bulk/#deleting-multiple-resources


                                              DELETE /articles
                                              Content-Type: application/vnd.api+json; ext=bulk
                                              Accept: application/vnd.api+json; ext=bulk
                                              
                                              {
                                                "data": [
                                                  { "type": "articles", "id": "1" },
                                                  { "type": "articles", "id": "2" }
                                                ]
                                              }

                                              Note: RFC 7231 specifies that a DELETE request may include a body, but that a server may reject the request. This spec defines the semantics of a server, and we are defining its semantics for JSON:API.
                                              0
                                              > Когда на сервере хранится сессия, и в зависимости от этой сессии приходят разные ответы, это нарушение принципа REST.

                                              Слегка не корректный тезис. И в целом это не так. Хотя подозреваю, что может зависеть от реализации.

                                              Например, разве это не разные запросы, которые должны давить разные ответы:

                                              /users
                                              /users/1
                                              /users/2

                                              ???
                                                0
                                                Предлагаю минусующим раскрыть свою точку зрения в ответом комментарии. Мне кажется это будет полезнее для общей эрудиции.
                                                  –1

                                                  А как отличие между /users/1 и /users/2 вообще связано с сессией пользователя?

                                                    0
                                                    Очень даже связанно) Пример показывает очевидный способ реализации постулата «запрос содержит все что нужно серверу для ответа». Именно поэтому сервер может быть stateless. Так вот, что происходит тут:

                                                    /users/1

                                                    Сервер понимает, что ему нужно отдать юзера с ID = 1, лезет в БД и достает всю необходимую информацию. Так?

                                                    Теперь посмотрим вариант с сессией:

                                                    /me + куках session_id

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

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

                                                    Основная мысль в том, что сессия — это такая же часть запроса, которая никак не влияет та то, является ли сервер stateless. Для простоты вынесем сессию из кук в get-параметры:

                                                    /users/1 — данные пользователя с ID = 1
                                                    /users/2 — данные пользователя с ID = 2
                                                    /users/null — ошибка, потому что в данном endpoint мы ожидаем ID юзера

                                                    /me?sid=lk235lk23lk5jl2k5 — данные юзера с сессией lk235lk23lk5jl2k5
                                                    /me — ошибка, потому что в данном endpoint мы ожидаем сессию

                                                    То есть, мы видим, что это разные запросы и ответы четко зависят от того, что пришло в запросе. Сервер никак не влияет на них, не хранит никакого скрытого состояния.

                                                    То что сессия скрытая от глаз и чаще всего хранится в куках, не делает сервер stateful. Наоборот, это способ временно делегировать стейт клиенту, без его участия, чтобы сервер мог оставаться stateless, а значит RESTful. Именно это позволяет серверу не хранить этот стейт у себя до следующего запроса.

                                                      –1

                                                      Вы упускаете из виду кеширование результатов запроса. Результаты запроса /users/1 могут быть кешированы какое-то разумное время. Результаты запроса /me + в куках session_id кешированы быть не могут...


                                                      Ну и семантика у таких запросов тоже разная. /users/1 — это данные пользователя 1, независимо от того кто их смотрит.

                                                        –1
                                                        И что? Это особенность реализации HTTP и разные виды параметров. Да, семантика передачи информации через path, query, header & cookie разные и работают не идентично, но это все равно передача информации.

                                                        Я лишь говорю о том, что запрос с сессией это такой же запрос к stateless серверу, как и без неё. И именно то, что один и тот же запрос отличающийся лишь наличием или отсутствием сессии возвращает разные результаты с сервера и доказываться что сервер истинно stateless.
                                                        –1
                                                        > Хранящаяся на сервера сессия, ниче в данном случае не отличается от любых данных там же.

                                                        В общем случае отличается. Кука, особенно сессионная — атрибут сеанса связи, атрибут конкретного агента, получаемый от сервера. Чтобы получить куку нужно сначала сделать запрос на сервер, а перенос кук ручками в общем случае рассматривается как атака.
                                                          –1
                                                          Сори, но вы тоже говорите не о том. Я написал «сессия хранящаяся на сервере», а не кука. Если данные сессии положить в базу, то вообще разницы как таковой нет.

                                                          Кука — это средство доставки параметра запроса. Один из множества вариантов, отличающийся спецификой клиента (браузера) и тем, что она управляется с сервера.

                                                          Но это никоим образом не отменяет того, что кука приходит к запросом. Без куки, сервер не вернет сессию, то есть сам по себе не хранит состояние. То есть опять же «вся необходимая информация для ответа, приходит в запросе».

                                                          Вы смотрели последний сезон «True detective»? Там он с утра встает и слушает то, что записал с вечера для себя на диктофон, потому что он не помнит. Это хорошая аналогия, чувак stateless и в курсе этого, поэтому он записывает себе куку, чтобы оставаться stateless, но иметь возможность «вспомнить» то, что нужно при следующем запросе.

                                                          Секьюрность, особенности кук и все остальное вообще к вопросу не имеет отношения никакого. Это вообще больше специфика браузерной среды, а не REST и HTTP. В этом всем вы правы.
                                                            0
                                                            Нет разницы, где хранится сессия на сервере. Если чтобы обработать запрос к конкретному ресурсу, в запросе должен быть идентификатор не только ресурса (URI), но и какого-то виртуального ресурса, который хранит в себе результаты предыдущих запросов конкретно этого агента (идентификатор сессии), то о stateless речи нет.
                                                              0
                                                              И это ключевой момент.

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

                                                              Другой вопрос, что определение дано неверное…
                                                    –1

                                                    Stateless означает, что сервер не хранит никакого состояния о сессии клиента на стороне сервера.


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


                                                    Это единственный способ масштабирования до миллионов одновременно работающих пользователей.


                                                    Полее подробно с картинками описано в диссертации Роя Филдинга.

                                                      –1
                                                      Все правильно написал, но тут нужно уточнить что ты подразумеваешь под «сессия»? Если интертификатор сессии в куке, тогда это как раз то, что передается на сервер при каждом запросе (пусть и не явно), то есть никаким образом не делает сервер более «stateful».

                                                      Если же речь о самих данных сессии, тогда нужно уточнять дальше. Хранится на сервере где? Локально на диске — это конечно блок для масштабирования, но в БД к специальной таблице — это другой случай. В конечном итоге из этой БД может читать любой инстанс сервера, поэтому это никак не может сказаться на масштабировании (масштабирование самой БД опустим, это не предмет разговора).

                                                      Резюмируя, сама по себе сессия и даже ее хранение на севрере никак не нарушает REST, но определенные способы ее применения и хранения могут в частных случаях его нарушать.
                                                        –1
                                                        Общепринятое понимание сессии я вижу как последовательность запросов и ответов в ходе одного сеанса работы пользователя. Данные сессии — данные, которые должны быть доступны серверу для корректной отработки последовательности именно как последовательности, когда результаты ответа на конкретный запрос зависят от предыдущих запросов и ответов.
                                                          –1
                                                          Написали очень обще, поэтому так и не понял вы оппонируете или соглашаетесь. Давайте рассмотрим лучше техническую реализацию и чем конкретно это может нарушать REST?
                                                            –1
                                                            Адский минусущий, который как пулемет минусует весь тред, будьте так добры, либо высказывайтесь сами, либо не мешайте высказываться другим.
                                                              –1
                                                              Любая реализация, где при запросе с клиента на сервер нужно передать любым способом идентификатор серверной сессии для, например, авторизации. Нарушение в том, что перед произвольным запросом необходимо сделать минимум ещё один запрос для открытия сессии и получения её идентификатора.
                                                                0
                                                                Если ресурс не доступен без какого-то дополнительного действия (в данном случае авторизации) — это не нарушение REST и не делает сервер «stateful». Это всего лишь часть бизнес-логики.

                                                                Если не согласы, тогда расскажите как это может повлиять на масштабирование и на возможность любого другого сервера/потока/инстанса, кроме того, который авторизовал юзера, обработать любой следующий запрос клиента? А ведь это главный стопер stateful-серверов — их сложно скейлить, потому что нужно гарантировать подключение к нужному инстансу, хранящему состояние.
                                                                  –1
                                                                  Нет, это именно нарушение REST и именно stateful сервер. Бизнес-логика может явно или неявно подразумевать наличие сессии, но где её хранить — это деталь технической реализации. В случае если она хранится на сервере, то это stateful сервер.
                                                                    0
                                                                    Делаю вывод что вы никогда не видели stateful сервер и не прописывали таблицы маршрутизации для таких серверов. Видимо и с проблемами балансировки на stateful серверах тоже не сталкивались.

                                                                    Если так рассуждать как вы, то тот факт, что на сервере вообще есть база и в ней лежит стейт, сразу делает его stateful. Да и на вопрос мой вы так и не ответили.
                                                                      –1
                                                                      Нет у вас оснований для таких выводов. Таблицы маршрутизации и проблемы балансировки к REST никакого отношения не имеют. Вернее REST подаёт как решение этих проблем stateless (в отношении сессий) сервера. Если вы используете серверные сессии, пускай и с расшаренными между серверами данными, то вы решаете эти проблемы, но не через REST-подход.

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

                                                                        Они имеют отношение к stateful серверам. Раз наш умозрительный сервер не нуждается в этом, значит он stateless. Уж простите, тут либо одно, либо другое. От ответа на вопрос, вы таки ушли, потому что прекрасно понимаете, что несмотря на сессию, сервер остается stateless (REST пока за скобками).

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

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

                                                                        В REST-подходе данные сессии хранятся на клиенте, он отправляет их на сервер с каждым запросом

                                                                        Можете предоставить цитату, где в трудах о REST написано про то, что все данные сессии должны каждый раз курсировать между клиентом и сервером? Как быть когда объем этих данных выйдет за размер куки (что бывает очень часто)?

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

                                                                        Наш умозрительный сервер, также может обработать любой запроса независимо и ему для обработки нужен только доступ к хранилищу ресурсов (БД).

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

                                                                          –1
                                                                          > Раз наш умозрительный сервер не нуждается в этом, значит он stateless.

                                                                          Очень узкое понимание stateless. Особенно в контексте принципов REST. Чисто технически, да, сервер (демон, сервис) stateless если он ничего в оперативной памяти не держит между запросами. Часто к этому добавляется пишет ли он что-то на ФС, но тут уже спорно: ФС — может быть примонтированной сетевой шарой, томом и т. п.

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

                                                                          Там написано «No client context shall be stored on the server between requests.». Общепринято клиентский контекст (под каким юзером залогинился, какой язык выбрал для сессии и т. п.) называть сессией.

                                                                          > Можете предоставить цитату, где в трудах о REST написано про то, что все данные сессии должны каждый раз курсировать между клиентом и сервером?

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

                                                                          > Наш умозрительный сервер, также может обработать любой запроса независимо и ему для обработки нужен только доступ к хранилищу ресурсов (БД).

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

                                                                          > Описанный вами вариант REST лишь на идеалогическом уровне может хоть как-то конфликтовать с тем, что пишу я.

                                                                          Нет, на вполне техническом. Если сервер определяет какую запись из БД показывать (и показівать ли) для URL /me на основании того какое имя пользователя и пароль были отправлены на /login, то это различие на вполне техническом уровне: ему нужно помнить что было отправлено и(или) какой результат обработки был.
                                                                            0
                                                                            Очень узкое понимание stateless.

                                                                            Нет, это просто очень практическое понимание. Любые дополнительные смыслы, которые вы накладываете не более чем идеология. Если сервер не имеет непереносимого состояния и может скейлится безболезненно на любое кол-во инстансов, значит он stateless по определению.

                                                                            Общепринято клиентский контекст (под каким юзером залогинился, какой язык выбрал для сессии и т. п.) называть сессией.

                                                                            Технически настройки языка должны храниться в базе. Но даже если говорить о сессии на верных примерах, данные сессии физически не хранятся на сервере. Они храняться на сервере БД. То о чем вы пишете валидно, если брать классический PHP-шный подход с сессиями на файлах. Но я даже не уверен, что кто-то его использует сейчас. Мы же не в 2000-х ей богу.

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

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

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

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

                                                                            запись из БД показывать (и показівать ли) для URL /me на основании того какое имя пользователя и пароль были отправлены на /login,

                                                                            Это не так. Сервер не пытается показать запись /me исходя из /login. Он знать не знает что клиент вообще когда-то ходил на /login, потому что это происходило в другом потоке или даже на другом физическом сервере. Текущий поток действует исходя исключительно из параметров запроса — пришел ID сессии «посмотрел» в базу, взял юзера. Не пришел ID сессии — «не посмотрел». А главное совершенно не важно откуда клиент взял этот ID. Хоть сам придумал.

                                                                            это различие на вполне техническом уровне: ему нужно помнить что было отправлено и(или) какой результат обработки был.

                                                                            Вообще не нужно ему это помнить. Еще раз, процесс логина 100% происходил в другом потоке, который уже почил со всем своим контекстом и на N% происходил на другом инстансе/сервере. При этом поток и сервер, который в данный момент обрабатывает /me не имеет у себя никаких рудиметнов от процесса логина и вообще знаний о существовании такого процесса.

                                                                              0
                                                                              Нет, это просто очень практическое понимание. Любые дополнительные смыслы, которые вы накладываете не более чем идеология.

                                                                              Практическое понимание простое: если для ожидаемого результата запроса нужно предварительно сделать ещё один запрос (типичный пример — запрос на аутентификацию), то сервер stateful в контексте REST. Второй запрос не содержит всей необходимой информации, раз первый нужно делать.


                                                                              Технически настройки языка должны храниться в базе.

                                                                              Никому они не должны. Базы вообще может не быть, кроме как сессионной.


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

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


                                                                              Это не так. Сервер не пытается показать запись /me исходя из /login. Он знать не знает что клиент вообще когда-то ходил на /login, потому что это происходило в другом потоке или даже на другом физическом сервере.

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


                                                                              Вообще не нужно ему это помнить.

                                                                              "сервер помнит, что юзер залогинен" в данном случае означает и "сервер знает или узнаёт, что юзер залогинен на параллельном сервер".

                                                                                +1
                                                                                если для ожидаемого результата запроса нужно предварительно сделать ещё один запрос (типичный пример — запрос на аутентификацию), то сервер stateful в контексте REST.

                                                                                Что значит «нужно»? Вот вам надо получить данные статьи, вы ID статьи подбирать будете научным тыком? Или вам сперва придется сделать запрос на список статей /posts, оттуда получить ID статьи и потом сделать запрос на статью /posts/:id? Исходя из вашей логики второй запрос также не возможен без предварительного первого запроса. Это тоже не REST теперь?

                                                                                Никто не заставляет клиент делать запрос на аутентификацию, но если ему нужно получить /me, сама бизнес-логика требует этого просто потому, что endpoint /me требует передать ID сессии в качестве параметра. Точно также как endpoint /posts/:id требует передать ID поста.

                                                                                Скажите, если запись будет такой /sessions/:id для вас это вдруг станет REST'ом?

                                                                                Никому они не должны. Базы вообще может не быть, кроме как сессионной.


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

                                                                                Кто вам сказал, что в случае хранения клиентского контекста на клиенте, он или его часть должна передаваться через куки?

                                                                                Ок, а как еще? Будете POST запросом каждый раз в body слать? А если вам нужно будет отправить запрос на создание какой-то записи и при этом нужно будет передать сессию? Вы как это себе видите?

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


                                                                                Во-первых, нет никакой «сессионной базы». В одной базе может лежать таблица Users и таблица UsersSessions со связью has-many или еще как-то. Во-вторых, в таблице UsersSessions не записано что юзер куда-то там ходил. Там просто либо есть запись по session ID, либо ее нет. Либо она просрочена, либо нет.

                                                                                Пишете какие-то откровенные глупости. Это тоже самое что сказать, что сервер полез в таблицу Posts и там в каком-то виде записано, что клиент ходил на endpoint создания поста. Ну бред же.

                                                                                «сервер знает или узнаёт, что юзер залогинен на параллельном сервер».


                                                                                WAT? Каком еще параллельном сервере? Он вообще не залогинен на сервере. Залогиненым на сервере можно быть, только если вы используете, ну я не знаю, HTTP Basic Auth что ли. Пользовательский запрос, либо имеет идентификатор сессии в системе в целом, либо не имеет. Никаких логинов «на параллельном сервере» не существует.

                                                                                Если я руками на клиенте пропишу другой sessionId и передам его серверу с запросом, сервер никогда не сможет понять, что это не моя сессия. Именно потому, что он не хранит состояния напрямую связанного с клиентом, а действует исходя из параметров запроса и только.

                                                                                  0
                                                                                  > Вот вам надо получить данные статьи, вы ID статьи подбирать будете научным тыком?

                                                                                  Гугл выдаст мне ссылку и я без подбора открою. Или пришлёт кто-то в слаке. А если я должен сделать запрос на /posts перед тем как открыть /posts/:id то это не REST.

                                                                                  > Скажите, если запись будет такой /sessions/:id для вас это вдруг станет REST'ом?

                                                                                  Да, если сессия будет открываться как POST /sessions и в ответ давать 201 Location: /sessions/:id. Ну и любые изменения в сессии будут осуществляться через PUT или PATCH /sessions/:id, а другие ендпоинты не будут давать 401 при отсутствии session_id в запросе

                                                                                    –1
                                                                                    Гугл выдаст мне ссылку и я без подбора открою.

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

                                                                                    Если серьезно, я вас спрашивал со стороны клиента. Если вы следите за конвой диалога, то мы как бы поисковые системы вообще не упоминали. Речь идет о том, откуда вы узнаете ID поста, если не из другого endpoint?

                                                                                    Да и про SEO нигде в REST не написано. Что там должно индексироваться поисковиками и как.

                                                                                    Да, если сессия будет открываться как POST /sessions и в ответ давать 201 Location: /sessions/:id. Ну и любые изменения в сессии будут осуществляться через PUT или PATCH /sessions/:id, а другие ендпоинты не будут давать 401 при отсутствии session_id в запросе


                                                                                    А с чего вы взяли что это не так работает? И чем таким пренципиальным отличается POST /login от POST /sessions? Итог операции один — создание новой записи в таблица БД. Никто кстати не мешает использовать для логина POST /sessions чтобы унифицировать апи. Так сессия сразу более REST стала? Ну ладно.

                                                                                    а другие ендпоинты не будут давать 401 при отсутствии session_id в запросе

                                                                                    Совершенно не связанные вещи. Бизнес-логика может требовать сколь угодного кол-ва параметров запроса, чтобы вернуть ресурс. Для некоторых ресурсов, которым нужен доступ к сессии, нужно передать session id и нет в этом никакого нарушения REST. Это лишь параметр запроса.

                                                                                    Опять же, если вам будет понятнее то вот это:

                                                                                    /me/:sessionId или /settings/:sessionId ничем не отличается от того же самого, когда sessionId лежит в куке. Только похитить его чуточку сложнее.

                                                      0
                                                      Ещё одна классическая ошибка, когда рассказывают про REST — ресурс !== таблица БД.
                                                        0

                                                        Да, конечно.
                                                        REST — передача состояния представления.
                                                        Естественно, представление может быть получено откуда угодно.

                                                          0
                                                          В таком случае вообще не понятны всевозможные relationships (считай join). Потому что по идее REST не запрещает создавать любое кол-во ресурсов под каждый клиентский случай. Более того, это даже лучше с точки зрения безопастности и производительности, чем query-rich api, то есть когда клиент может составлять сложные запросы как его душе угодно.

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

                                                          Опять же супер частая ситуация: проектировщик БД умный парень, заделал все с расчетом на будущее, таблиц куча, куча ключей, индексы и все по феншую. Тупое маппирование апи на таблицы станет причиной самоубийства вашего фронтендера. Но вдруг оказывается, что не смотря на то, что условный «BlogPost» состоит из самого материала, его автора, комментариев, какой-то мета-информации, информации о тематике и разделе, а также лайках и шерах, и все это лежит в разных таблицах, но на клиенте, эти данные практически не используются раздельно. Оказывается не нужны нам все эти:

                                                          /posts?query
                                                          /comments?query
                                                          /categories?query
                                                          /etc

                                                          Достаточно выдать клиенту некий ресурс «post», который уже включает в себя всю предметную область. Фронтенд доволен, бекенд доволен, потому что отвязал REST-ресурсы от таблиц БД и теперь у него развязаны руки.
                                                            0
                                                            Нет никакой связи с БД.
                                                            Relationships (считай join) связывает представления, которые могут быть получены откуда угодно.
                                                              0
                                                              Почему не сделать новый ресурс, который очевидно для бекендера свяжет нужные сущности и предоставить фронтенду ровно то, что ему нужно?
                                                                +2
                                                                Сделай, спецификация JSON API это не запрещает. :) Она лишь говорит, что «если Вам нужны связи между представления, то вот так их можно сделать».
                                                                  0
                                                                  А ну ок.)) Из доклада мне почему-то показалось, что это как sql join своеобразный.
                                                                  0
                                                                  Чтобы не дергать бэкендера каждый раз, когда фронтендеру нужно что-то ещё или то же самое, но немного по другому, или больше не нужно. Так же чтобы не плодить ендпоинты в геометрической прогрессии по количеству связей.
                                                                    0
                                                                    Это отличная сказка. Другая крайность GQL, в котором клиент может делать с сервером практически что ему хочется. Лично я считаю, что бекенд слишком важен, чтобы опрометчиво давать столь широкий доступ для фронта. А апи они на то и нужны, чтобы работать на нужны своего юзера, поэтому дописать endpoint это норма.

                                                                    Если конечно вы не делаете некий public api для неограниченного круга лиц, тогда соглашусь, надо давать больше свободы. Но мы ведь вроде не про это.
                                                                      0
                                                                      Не практически что ему хочется, а то что дозволено. И да, как раз про принципу «апи они на то и нужны, чтобы работать на нужны своего юзера» лучше написать универсальный ендпоинт, чем плодить десятки и сотни их на каждый кейсы типа «юзер захочет посмотреть статистику по продажам в разрезе регионов», «юзер захочет посмотреть статистику по продажам в разрезе городов», «юзер захочет посмотреть статистику по продажам в разрезе городов, но сгруппированных по регионам», «юзер захочет посмотреть статистику по продажам в разрезе групп товаров», «юзер захочет посмотреть статистику по продажам в разрезе регионов и групп товаров» и т. д., и т. п.
                                                                        0
                                                                        Извините, тут я с вами спорить не буду, потому что вы поднимаете вопрос идеалогии. Вам так больше нравится — ок.

                                                                        Лично мне не кажется, что есть способ написать безопасный, хорошо оптимизированный один endpoint на все случае жизни. По крайнем мере, ни у кого это сделать пока не получилось. Поэтому если я хочу, чтобы мои сервера были безопасными и быстрыми, я готов делать отдельное представление для наборов данных. Кроме того, это еще и довольно дешево. Если внутренние механизмы доступа и работы с данными реализованы грамотно, то создание такого представления занимает отсилы 1-2 дня, потому что это лишь композиция существующего функционала.

                                                                        Опять же речь не о RPC, где могут существовать методы типа getStatsByCities, речь о том что в архитектуре REST ресурс !== таблица БД, а полностью универсальный сервер можно написать только, если принять это равенством. Самое забавное, что даже в этом случае написать решение на все случае жизни все равно не выйдет.

                                                                        p/s Еще раз уточню, что исключение могут составлять некие публичные апи, у которых цель работы — обслуживание неопределенного кол-ва клиентов, реализующих неопределенное кол-во задач. Тут наверное, разработчикам просто приходится искать максимально универсальные решения, потому что это часть задачи.
                                                                          –1

                                                                          Это не идеология, это голый прагматизм: делать новый ендпоинт или расширять имеющийся только когда фронту нужны новые данные, а не просто новое сочетание тех, которые и так ему уже доступны, пускай и немного в другом виде. При условии, конечно, что реализация универсального (в рамках архитектурной модели) API проще чем реализация 100500 отдельных ендпоинтов на каждый каприз пользователя.


                                                                          Полностью универсальный сервер точно не получится сделать приняв ресурс === таблица БД, потому что табличные БД и вообще БД имеют свои ограничения.

                                                                            0
                                                                            При условии, конечно, что реализация универсального (в рамках архитектурной модели) API проще чем реализация 100500 отдельных ендпоинтов на каждый каприз пользователя.

                                                                            Ключевой момент. В том то и дело, что не проще. А уж насколько он не проще в поддержке, это и представить сложно.

                                                                            Полностью универсальный сервер точно не получится сделать приняв ресурс === таблица БД, потому что табличные БД и вообще БД имеют свои ограничения.

                                                                            Только так и получится сделать, потому что вы фактически вынуждены маппировать апи на sql (или иной язык запросов), то есть придумываете свой язык запросов поверх http. Что и сделали GQL. Если же вы не примете что ресурс === таблица БД, то в какой-то момент необходимый клиенту запрос будет просто невозможно осуществить с помощью вашего универсального endpoint.
                                                                              0

                                                                              Выставляем наружу не реляционную структуру базы, а объектную (предположим что ООП) модель, которая где-то под капотом маппится на базу посредством ORM.

                                                                                0
                                                                                Ну и какая разница? В ORM обычно модель напрямую связана с конкретной таблицей. Вы таким образом только утилитарные таблицы для связей many-many исключаете из апи. Остальные таблицы по сути станут ресурсами.

                                                                                Если вас это устраивает, то можете попытать счастье в попытке сделать апи для всех случаев жизни. Из моего опыта — это не возможно.
                                                                                  –1
                                                                                  Напрямую с какой-то таблицей модель связана только в двух паттернах ORM из трёх, причём один из двух очень редко встречается (я лично не встречал ни разу). Собственно суть ORM в том, чтобы развязать объектную и реляционные модели, чтобы о таблицах знал только ORM.
                                                                                    –1
                                                                                    Больше похоже на жанглировние понятиями чем на довод. В итоге Entity ORM, например, Posts, все равно маппируется на таблицу posts и ничего тут с этим не сделаешь.

                                                                                    Этот тред прекращаю, потому что он ушел вообще не туда. Хотите создавать унифицированные апи под любые цели — флаг в руки и удачи. Она понадобиться.

                                                                                    Лично я за максимальный контроль со стороны бекенда и за то, чтобы ресурс представлял данные так, как их «видит» клиент, а не БД.
                                                          0
                                                          В целом и первый вариант вполне может удовлетворять изложенным принципам REST и его можно описать как «RESTful API, использующий HTTP в качестве тупого транспорта». То, что излагается дальше можно описать как «RESTful API, использующего все преимущества и семантику HTTP, как REST-протокола „

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

                                                          Самое читаемое