Как стать автором
Обновить

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

Главная проблема REST в том, что REST — это термин, не стандарт, не спецификация, а всего лишь свод соглашений, весьма условных и слабых. Каждый волен пилить свою API как угодно. Сколько копий сломано в спорах о том, как называть маршруты до сущностей, правильно ли включать в маршруты глаголы, как версионировать, должен ли API отражать только взаимодействие с сущностями (CRUD) или может обрабатывать бизнес-операции? Базовый CRUD может написать каждый даже не имея навыков программирования, благо сейчас полно конструкторов API. Но стоит API чуток разрастись, как тут же начинаются проблемы с бизнес-процессами. Например, как сделать API для добавления комментариев? POST /comments? POST /articles/1/comments? COMMENT /articles/1?


Многие возразят, что есть стандарты для реализации REST API вроде Swagger (Open API), JSON:API, OData, HAL, RAML, HATEOAS и т.п. На практике из этих стандартов жизнеспоспобны (на моем опыте) только Open API и RAML. И несмотря на наличие этих стандартов все равно остается проблема общения с потребителями API (клиенты, фронтендеры). В лучшем случае у потребителя будет HTML-документация (счастье, если не устаревшая).


Коммент получился сумбурный, но что я хочу сказать из своего опыта — REST хорош для начала как MVP или как публичное API чисто для фронта, потому что можно пользоваться всеми фичами HTTP. Для чего-то серьезного рекомендую применять GraphQL, не потому что он хороший, а потому что на данный момент достойных альтернатив нет.

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

В силу специфики своей работы я часто использую JSON-RPC, который тоже имеет лучшие практики, но от которых часто отклоняются разработчики…

Все статьи заканчиваются описанием простейших случаев. Да и то не отвечают на некоторые вопросы если копнуть глубже. Например если в базе не ещё ни одной статьи что вернуть? Вариант наиболее естественный пустой массив. Однако orm часто в этом случае возвращает null или undefined, вследствие чего это значение попадает и на выход api. Тут же обязательно найдется теоретик который скажет что статей нет значит нужно вернуть 404 статус. По пагинации аналогично. Автор както запамятовал что нужно передать ещё общее количество записей в базе для организации интерфейса. Как их передавать? В заголовке вдруг и внезапно. Или делать объект с полями count, items? Или если у вас в статье есть поле Автор. И нужно передать из связанной таблицы имя и фамилию. Должен ли ответ включать autorId или только объект author с полемid? А если мы обновляем автора то мы должны прислать новый идентификатор в поле autorId или в объекте author { id

Однако orm часто в этом случае возвращает null или undefined, вследствие чего это значение попадает и на выход api.


return $posts ?? [];

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


на laravel делаю так:
return $query->paginate();

paginate возвращает массив с данными data, count, page и так далее

На фронтенде для этого объекта описан интерфейс метод получения постов будет возвращать PaginationInterface<Post[]>

Или если у вас в статье есть поле Автор. И нужно передать из связанной таблицы имя и фамилию. Должен ли ответ включать autorId или только объект author с полемid?


Тут логичнее 2 вариант.

А если мы обновляем автора то мы должны прислать новый идентификатор в поле autorId или в объекте author { id

Если все что вернется — это id, тогда логичнее возвращать int
А почему OData не жизнеспособна? Что Вы имеете в виду?
Это ведь протокол, а не свод соглашений.
Не знал про GraphQL, интересная разработка. Век живи век учись!

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


Например запрос "добавить пользователя в проект". Что это должно быть с точки зрения методов HTTP? Post? Put? А какую ошибку он должен выдавать, если у этого пользователя, например, нет прав на доступ к этому проекту? 403 Forbidden? А как это отличить еще от кучи возможных ошибок доступа (например, у нас самих нет прав добавлять пользователя в этот проект, или нет прав вообще работать с пользователями и вообще нет доступа к этому эндпоинту, а может у нас вообще прокся сглючила или сеть отвалилась и запрос даже до сервера не дошел)?


Поэтому в большинстве проектов что я видел использовался какой-то JSON-over-HTTP, который вряд ли можно назвать RESTом, но все называли его так. Сервер всегда возвращает 200 ОК, HTTP ошибки используются только для реальных ошибок протокола и сети. Внутри ответа JSON с реальным кодом ошибки. Из методов обычно используется только GET (для запросов не изменяющих состояние) и POST (изменяющих). Ну иногда DELETE. Эндпоинты именуются как захочется, чтобы было понятно, а не в угоду какому-то синтетическому стандарту.


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

“ Например запрос "добавить пользователя в проект". Что это должно быть с точки зрения методов HTTP? Post? Put?”


Это post


« А какую ошибку он должен выдавать, если у этого пользователя, например, нет прав на доступ к этому проекту? 403 Forbidden? А как это отличить еще от кучи возможных ошибок доступа (например, у нас самих нет прав добавлять пользователя в этот проект, или нет прав вообще работать с пользователями и вообще нет доступа к этому эндпоинту, а может у нас вообще прокся сглючила или сеть отвалилась и запрос даже до сервера не дошел)?»


Да, forbidden, а отличить легко, посмотреть код ошибки, Не важно каких прав нет, статус всегда 403 (если ошибка относится к правам), но в ответе разработчик дополнительно указывает данные, message, code и так далее, всяко лучше чем когда все get и не возвращает ошибок как в вашем примере ниже

НЛО прилетело и опубликовало эту надпись здесь

Вот спорно очень насчёт кодов ошибок. Потому как, к примеру, 404 — относится к тому что нет маршрута или нет документа? 50x — проблема с транспортом, прокси или самим приложением? Если не смотреть внутрь — то по одному коду непонятно, это проблема endpoint или самого приложения, а если смотреть и разбирать — то какой смысл использовать коды транспорта?


Или в приведенном примере:


если мы пытаемся передать полезную нагрузку со значением email, уже присутствующим в users, то получаем отклик с кодом 400 и сообщение 'User already exists', означающее, что такой пользователь уже существует

А если сообщения локализованы? Как на уровне клиента корректно это обрабатывать, имея только код 400? Если есть ещё код от самого приложения — то см. выше, коды транспорта просто лишены смысла — неясно, ошибся пользователь, указав неверные данные, или приложение где-то словило исключение по независящим от пользователя причинам.


Яркий пример такого неудобства — это PowerDNS API — там на всё код 400 и исключительно текстовые сообщения об ошибках, хорошо хоть не локализованы — но если кто-то решит их изменить хоть на один символ (что уже случалось), то все парсеры пойдут в лес (да, как это не удивительно, но некоторые клиенты создаются с учётом адекватной реакции на ошибки).


Из кода ошибки должно быть ясно сразу и однозначно, где и почему она произошла (транспорт/приложение/валидация/etc), она временная или нет, одна или много, имеет ли смысл повторить запрос позже или "уже всё" и т.д. (примерно как SQL state). Очевидно, что HTTPшных кодов для этого недостаточно и поэтому использовать их для этого мало смысла.


Я обычно при проектировании REST API исхожу из того что все коды ошибок HTTP относятся исключительно к транспорту (т.е. собственно HTTP), и при любом раскладе, если документ попал в приложение и был там обработан, то на выходе должно быть 200 — при любом ответе, с ошибкой или нет, а само тело ответа всегда содержит подробную информацию в случае ошибки в структурированном виде. Если код не 200 — значит он не попал в приложение, со всеми вытекающими.


Из личного опыта — у одного из моих клиентов, которые использовали "чистый REST", пользователи никогда (при первой реализации) не изучают тело ответа при коде отличном от 200, были потрачены тонны тикетов для объяснений почему они должны смотреть внутрь если это не 200. Что примечательно, при переходе на код 200 в следующей версии с ошибками внутри никаких вопросов у новых пользователей уже не возникало.


И наконец, как уже выше заметили, REST это принцип и архитектура, а не спецификация или стандарт, и соответсвенно транспортом когда-нибудь может оказаться вовсе не HTTP — вот почему не стоит делать маппинг ошибок на уровне транспорта — приложение всё равно должно качественно о них сообщать.

Получение текстовых строк 200 { error: "User already exists" } действительно с системой локализации не матчится и подходит только для неожиданных сбоев, поэтому во всех проектах делаем константы 200 { error: "USER_EXISTS" }, фронт уже выполнит необходимую логику и заберет из локалей перевод. С остальным согласен, HTTP коды только для транспортных ошибок удобны, а те, кто на них опирается в разработке сложных приложений, сталкиваются с их многозначностью, которую приходится специфицировать либо в body, либо в заголовках ответа (типа 402 resp.headers { validation_error: "FIELD_NOT_VALID" }).

НЛО прилетело и опубликовало эту надпись здесь

Нет, есть. В одном случае это ошибка в приложении, в другом — ошибка данных.
Вероятно надо при этом разные сообщения об ошибках выдавать.
Или вот пример выше я приводил — получаете 403, а что вы должны показать клиенту? Есть куча ошибок бизнес-логики, попадающих под определение ошибки доступа, возможно на клиенте их надо обрабатывать по-разному, но из кода ошибки это так просто не выжать.


При этом я точно встречал библиотеки работы с HTTP, которые в случае ошибки от сервера выкидывали всякие эксепшены и не давали заглянуть в тело ответа. Так что идея "код 403 + json внутри с подробным описанием" даже технически работает не всегда.

Интересно что в обсуждении оригинала статьи те же самые контрпримеры. Хотя пожалуй сейчас это уже мало кого волнует. Все уже смирились. А с развитием микросервисной архитектуры, построенной на HTTP протоколе просто не имеют альтернативы. Если не REST-API то нужно забыть про микросервисы, kubernetes — а это как раз наоборот сейчас самые перспективные как думает большнство архитектурные решения и технологии.


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

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

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

Строго говоря, два неравноправных случая. Отправка почты – это создание письма, буквочка C из CRUD. Поэтому это будет что-то вроде POST /mail.
А вот активация пользователя – это уже действие. Поэтому тут каждый извращается, как может.

Активация это U, потому что пользователь уже существует и де-факто это изменение части объекта. Логично для этой цели использовать PATCH.

и де-факто это изменение части объекта

Не всегда действие ведет к изменению объекта.
Логично для этой цели использовать PATCH.

Это же ни разу не очевидно. Т.е. на PUT вы пользователя сохранили (и это общепринятая практика), а на PATCH почему-то активировали. А если еще будут действия с пользователями что вы будете делать?

POST — создать новый объект. PUT — заменить существующий объект. PATCH — изменить часть существующего объекта. Что тут неочевидно?


Если у объекта есть атрибут "активен" — это часть объекта, так что на любые изменения отдельных частей — это таки PATCH.


Если уж вам хочется что бы всё было отдельно — сделайте user-activation и используйте PUT или POST, хотя это (как мне кажется) менее логично, потому что сразу возникает искушение создать его отдельно от user (и кто-то обязательно это сделает, пусть и неудачно).

PUT — заменить существующий объект. PATCH — изменить часть существующего объекта. Что тут неочевидно?

Мы друг друга не поняли. Я действительно имел в виду, что активация это не только изменение свойства у пользователя, а отдельный большой процесс.
сделайте user-activation и используйте PUT или POST

ниже уже про такое писал, но лично мне нагляднее видеть
POST user/{id}/activate

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


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

Одним и не самым маловажным отрицательным свойством restapi является в частности тот факт что отсутствие стандартизации и спецификации по любому вопросу отличному от crud порождает бесконечные споры. Что мы можем наблюдать даже на примере этого вот обсуждения. Главное что все позиции правы. И не правы. Не правы в том что пытаются найти смысл в restapi

Я думаю, что автор вопроса под "активацией пользователя" имел в виду нечто большее, чем, к примеру, установку свойства isActive из false в true. Под активацией может подразумеваться целый ворох изменений, не обязательно одного свойства. К тому же, активация может генерировать цепочку событий, на которые подписаны другие части системы.

Никто не запрещает действия при изменении свойства, этот ворох изменений может быть сделан во время PATCH при изменении состояния свойста, с соответствующим фидбеком (получилось/нет).


С другой стороны, точно также никто не запрещает создать отдельный ресурс для этого, который будет отличаться только… отдельным ресурсом, в то время как с точки зрения клиента принципиально ничего не меняется — объект активирован, что там произошло для этого "у ней внутре" его не интересует, кроме подтверждения изменения состояния.


Если же активация подразумевает отдачу чего-то очень интимного, свойственного только активации (credentials, token etc) — да, в этом случае уже будет логично создать отдельный ресурс.

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

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

Один из подходов – всё-таки иметь wite-only ресурсы, имена которых обозначаются не существительными, а глаголами: POST /user/{userID}/activate

В статье написано, что глаголы нельзя. Часто видел когда люди, следуя подобным правилам, делали отглагольные варианты, например
POST /user/{userID}/activating или activation
Выглядит странновато, лично мне привычнее глаголы, но люди говорили что глаголы нельзя, правда, не могли объяснить почему.

То что написано в статье — одно из мнений. Делайте так как вам подсказывает команда, здравый смысл, клиенты и ТЗ.


REST — это не стандарт и не спецификация, никто вам не может указывать что "можно" а что "нельзя", хотя если пользователи станут возмущаться — к ним всё же стоит прислушаться, потому что вы делаете это для них.


Идеальный API — это тот когда пользователи активно его используют и при этом у них (почти) не возникает вопросов — исходите из этого.

Я конечно понимаю что статья про rest но хочу напомнить что http это не единственный способ организации взаимодействия, есть еще вебсокеты. Я вот например выбросил http и перевел все взаимодействие клиента с сервером на вебсокеты и не нарадуюсь — мне стали не нужны все эти бэкенд-фреймворки (например expres, koa, nestjs) и библиотеки которые в основном нацелены на http-стек.
К тому же напомню что в области разработки десктопных приложений для взаимодействия клиента с сервером испокон веков использовали обычные сокеты. И только с появлением веба и потому что до некоторого времени в браузерах javascript поддерживал лишь отправку http запросов стали популярными все эти rest-подходы поверх http.
Но теперь когда у нас есть поддержка вебсокетов (а это не еще одна абстракция поверх http как бывают думают некоторые, да для установки соединения по вебсокетам используется http но дальше это просто передача хедера с размером сообщения поверх tcp) и растет популярность desktop-like web-приложений (а есть еще offline-first приложения которые могут работать без сети и нужно синхронизировать изменения) использовать http в качестве транспорта не имеет никакого смысла

На 95% процентов согласен. Единственное — если данные передаются редко, вебсокеты либо станут отключаться, либо (если есть какой-то keep-alive) будут создавать нагрузку на сервер.

100% с Вами согласен. По советам и нагрузке. Однако если вынести советы на какой то брокер то нагрузка может не только увеличить вы но даже уменьшиться. Так как не будет затрат на соединение и загрузка будет более сглаженная. Так как бизнес сервер будет выбирать из очередей события в том объеме ко оный может обработать

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

Вот Вы все вроде бы говорите правильно. Но есть нюансы.
По порядку. Брокер нагрузку сглаживает не в большей степени чем любой веб-сервер у которого запросы выстраиваются в очередь. Ускорение происходит за счет других факторов. Нет расходов на повторное соединение и нет влияния мелких разрывов в сети. Так как согласно протокола произойдет реконнект и ответ с сервера будет получен.
Опрос по таймеру. Честно, иногда приходится соглашаться. Но по двум причинам. Если заведомо известно что трафик будет очень слабый, и так как все уже реализовано по restapi и тянуть еще одну технологию мобильные разработчики просто отказываются наотрез.
По пушам firebase все классно. Было бы. Если бы их нельзя было отключать. Но их иногда отключают и что же ломать логику приложения?
По хорошему с таймером н и когда и не при таких условиях.
Ну и про erlang. Брокеры пишут на erlang. Их довольно много.

Вы считаете что постоянно держать соединение дорого? Соединение в linux и в node это всего лишь файловый дескриптор размером с сотню байт. Расходов кроме памяти практически нет (браузеры либо вообще не отсылают специальные "ping"-вебсокет сообщения либо делают это очень редко — за полчаса я не получил ни одного такого сообщения, а потом мне ждать надоело)


А что касается http — в нем есть заголовок "connection: keep-alive" который сообщает серверу открыть и держать tcp-соединение (и пересылать все http запросы по этому соединению) точно так же как и с вебсокетами, плюс в версии http 1.1 это подразумевается по умолчанию (если явно не передано "connection: close"). В общем можно сказать что значительная часть интернета использует keep-alive. В таком случае у http нет никаких преимуществ перед websockets


Даже наоборот — в http есть фундаментальный недостаток — из-за того что http параллельный и "stateless" — браузеры не гарантируют что запросы на сервер поступят в том же порядке в котором были отправлены на клиенте (могут использовать keep-alive а могут и не использовать — это всего лишь оптимизация и узнать и проконтролировать со стороны javascript невозможно) и из-за этого на порядки усложняется решение одной из самых главных проблем всех бэкендов — https://habr.com/ru/company/yandex/blog/442762


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

Принимаем JSON и выдаем JSON в ответ

В смешанной среде полезней иметь как раз стандартные multipart/form-data для POST'а cURL'ом например.

стоит ли делать композитный апи над enity апи ?

401 Unauthorized

Тут надо заметить, что этот код при отсутствии корректной аутентификации используется только потому, что лучшей альтернативы нет. Unauthorized — это отсутствие авторизации, а не аутентификации. Отсутствие авторизации что-то делать вызывает запрет, т.е. Forbidden — так что по смыслу словесной части 401 и 403 скорее идентичны, чем различны. Правильно было бы написать 401 Unauthenticated, но что уж есть в RFC — то есть.
В книжках по SQL я как раз встречал совет называть таблицы в единственном числе. Типа все и так знают, что в таблицах обычно хранят несколько значений. Тогда и в REST API стоит использовать единственное число.
В итоге получаем экономию на трафике за счёт избавления от [E]S в каждом запросе.

Лучше JSON заменить на что-нибудь вроде MessagePack или CBOR, а это так копейки.

Rest более менее пришёл в порядок последнее время, и лишь из года в год поднимается тема "REST зло, дайте RPC".


А вот дальше общего понимания — чехарда, как возвращать объекты/коллекции, как ошибки, как пагинацию, как связи, как перелинковку… вот тут пока сплошное творчество и гайдлайнов мало.

Не могу понять как он пришел в порядо если через двадцать лет все смотрят о тех же вопросах. Появился open api. Да это хорошо. Но это же просто средство документирования и отчасти проектирования. Хотя больше документтирования

RESTful как набор архитектурных принципов кажется простым, но реализовать протокол довольно сложно. Например, вот попытка формализации REST поверх HTTP в виде машины состояний https://github.com/webmachine/webmachine/wiki/Diagram. Без поддержки и жёстких ограничений со стороны фреймворка, на практике такое ни кто делать не станет. Поэтому реальные API весьма разнообразны.

Rest это набор архитектурных принципов. А restfull он же reatapi это практика истоки которой лежат в скафолдинге первых фреймворков. Когда некие фронтовые запросы прямо отражались в запросы к базе данных

да, спасибо, речь конечно же про REST. а RESTful это максимальный уровень соответствия принципам/ограничениями REST.

Restful api не имеет никакого отношение к rest и во многом ему противоречит

Интересно, если это распространенное мнение, можете кинуть ссылкой где про это почитать, или привести какие-то примеры?

А что скажите по поводу использования OpenAPI, swagger и генерации серверного кода?

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

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


В этом смысле довольно полезна http://apistylebook.com/


Сборник гайдов разных компаний с группировкой по разделам.


Даёт возможность
1)посмотреть "а как у них"
2)узнать об аспектах/проблемах которые в co-статьях редко освещаются, а на старте проекта могут быть не видны.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий