Pull to refresh

REST/CRUD. Я неправильно его готовлю?

Programming *

Вступление


REST — очень интересный метод для работы с объектами на сервере. Реализация CRUD-интерфейса средствами REST проходит легко и просто! Сегодня я попытаюсь показать какие из подходов в REST/CRUD по моему мнению ошибочны и пагубны для проекта.

Must have для всех ресурсов


Для продолжения повествования стоит сразу же определиться с минимальным набором свойств объекта, адресацией и прочими прелестями. Начнем:
  • URL — это такая штука, перейдя по которой можно 100% получить информацию о текущем состоянии екземпляра объекта. Имеет вид
    <protocol://><server:port>/<path>/<resource>/<slug>
  • URN — это такая штука, перейдя по которой можно 100% получить информацию об объекте. Отличается от URL тем что не привязывается к конкретному протоколу и серверу. В 99 случаях из 100 имеет смысл использовать что-то одно. Имеет вид
    <path>/<resource>/<slug>
  • URI — это такая штука, сравнивая которые между объектами можно 100% утверждать работаете ли вы с экземпляром одного и того же объекта. Иными словами если для 2-х и более объектов верны их URI, то это один и тот же объект! Имеет вид
    <resource>/<slug>
  • resource — это такая штука, сравнивая которые между объектами можно 100% утверждать работаете ли вы с экземплярами одной и той же коллекции объектов. Иными словами если для 2-х и более объектов верны их resource, то это объекты из одной и той же коллекции!
  • slug — это такая штука, сравнивая которые между объектами без привязки к resource можно 100% утверждать что Вы делаете что-то не так. Иными словами если для 2-х и более объектов верны их slug, то это НИЧЕГО НЕ ЗНАЧИТ! Служит исключительно для однозначной идентификации экземпляра в коллекции!

Важное отличие slug от id

  • id — целочисленное значение которое НИЧЕГО не значит для пользователя
  • slug — осмысленное строковое значение. Как правило с символами подчеркивания (или точки) вместо пробелов

Для sql-танкистов (без обид) поясню:
  • id в таблице — для целостности данных в отношениях. Имеет ограничения NONULL, UNIQ, INT
  • slug — для API. Имеет ограничения NONULL, UNIQ, CONST и НЕ ИСПОЛЬЗУЕТСЯ в отношениях.

Например users.id — это id, а вот users.login — вполне себе slug.

А теперь принимаем за аксиому «все объекты должны иметь указаные свойства», и, чтоб не размазываться по объекту запихиваем их например в свойство "__link__":
{
    __link__: {
        url: 'http://localhost/api/users/vasya',
        urn: '/api/users/vasya',
        resource: 'users',
        slug: 'vasya'
    }
}

Кстати, это — валидный объект. Он мог вернуться с какого нибудь url (не «localhost/api/users»). Разработчик, получив такой объект, должен запросить данные по url/urn из __link__, но об этом позже.
Кстати, «по правильному» — не вносить доп. свойство "__lnk__", а использовать для этих целей заголовки формата X-URI, X-URN, X-URL, etc, но не всем веберам нравится работать с заголовками. Мол взял body и побежали.
Хммм… а сколько реализаций REST API имеет подобные свойства? Может я изобрел велосипед?

CRUD/Read или HTTP/GET


Тут все просто. Берем URL, запрашиваем объект, и… А что «и»? И иногда наблюдаем вот такой ответ:
Пример:
HTTP/GET http://localhost/api/users

{
    status: true,
    count: 100,
    data: [
        {
            login: 'vasya',
            name: 'Vasilii',
            id: 146,
            __link__: {
                url: 'http://localhost/api/users/vasya',
                urn: '/api/users/vasya',
                resource: 'users',
                slug: 'vasya'
            }
        },
        {
            login: 'petya',
            name: 'Petr',
            id: 145,
            __link__: {
                url: 'http://localhost/api/users/petya',
                urn: '/api/users/petya',
                resource: 'users',
                slug: 'petya'
            }
        }
    ]
}

Что тут не так:
  • Зачем тут «status»? HTTP/200 не хватает? Или ваш сервер не умеет передавать текст сообщения об ошибке? Отправка HTTP/200 при «status == false» — сокрытие ошибки. Если бекенд не имеет логирования в точке возникновения ошибки — будем долго дебажить в поисках что же происходит. Тогда как лог http сервера сразу скажет на каком url произошла ошибка.
  • Подобный «wrapping» заставляет веберов писать «unwrap-функции» для вывода сообщения об ошибке или получения данных. Хотя для полноценного дебага достаточно будет проблемного URL.
  • Хм… А если элементов +100500, и кто-то из админов сменил свойство «name» для пользователя «petya»? Или, что чаще, в коллекцию добавили нового пользователя и count уже 101? Правильно, про HTTP/Cache-control можно забыть — будем тянуть весь список по новой.

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

Ответ должен быть таким:
[
    {
        __link__: {
            url: 'http://localhost/api/users/vasya',
            urn: '/api/users/vasya',
            resource: 'users',
            slug: 'vasya'
        }
    },
    {
        __link__: {
            url: 'http://localhost/api/users/petya',
            urn: '/api/users/petya',
            resource: 'users',
            slug: 'petya'
        }
    }
]

При этом count должен жить в HTTP/HEAD, т.к. не имеет на прямую отношения к ресурсу, а характеризует состояние коллекции. Опят же, Вам надо вывести кол-во пользователей. Как быть? Выбирать все и считать в JavaScript? Писать новый url для получения кол-ва элементов? Зачем? Сделайте HTTP/HEAD запрос к колекции. Данных не будет, вернутся только зголовки.

Скептики скажут: для отрисовки всех элементов придется делать множество запросов к бекенду,
Отвечу: HTTP/1.1/Keep-alive спасет нас всех. При использовании javascript-фреймворков и кеширования — большинство данных будут запрошены при инициализации, разово, а после — будут только обмениваться запросами с ответом HTTP/304 (ресурс не изменился).

Знаете почему веберам не нравится такой подход? Им приходится контролировать получение данных с сервера для нормального отображения.
Например: Если данные прилетают сразу со свойствами — можно сразу отрисовать несколько дивов в цикле не заботясь о синхронизации. Если же делать 4 запроса для вывода 4-строк с пользователями — надо как-то отслеживать состояние всех запросов. Т.е. пока ВСЕ запросы не отработают — ничего не рисовать. См. Promise и Future для решения подобных проблем. Они все имеет код для принудительной синхронизации, хотя я бы рисовал сразу как есть…

P.S. На часах 2:30. Очень хотелось рассказать про вложенность ресурсов, про массовый DELET/POST/PUT… но я пожалуй пойду спать. Оставлю на суд общественности данный опус. Прошу высказываться в комментариях — стоит ли продолжать или прописные истины не в моде?
P.P.S. Был бы очень признателен людям за разные грабли на которые пришлось наступить при работе с REST API от сторонних разработчиков.
Всем спасибо!
Tags:
Hubs:
Rating 0
Views 17K
Comments 3
Comments Comments 3

Posts