Какие основные ограничения у REST API?

Ответ

При проектировании и использовании RESTful API на Node.js я сталкивался со следующими ограничениями:

  1. Over-fetching и Under-fetching:

    • Over-fetching: Клиент получает больше данных, чем нужно. Например, эндпоинт /api/users/123 возвращает все 20 полей пользователя, а клиенту нужно только имя и аватар.
    • Under-fetching: Клиенту не хватает данных, и он вынужден делать несколько последовательных запросов. Чтобы построить профиль страницы, может потребоваться запрос к /users/123, затем к /users/123/posts, потом к /posts/456/comments.
    • Решение в Node.js: Можно использовать библиотеки для частичных ответов (fields query parameter) или рассмотреть GraphQL.
  2. Отсутствие встроенного реального времени (Real-time): REST основан на HTTP-цикле "запрос-ответ". Для чатов, уведомлений или live-обновлений нужны отдельные технологии.

    • Решение в Node.js: Использовать WebSockets (библиотеки Socket.IO, ws) или Server-Sent Events (SSE) поверх HTTP.
  3. Жёсткость эндпоинтов и версионирование: Структура данных и эндпоинты фиксированы. При изменении API (например, переименовании поля) приходится поддерживать старые версии (/api/v1/users, /api/v2/users), что усложняет код.

  4. Сложность описания сложных операций: Иногда операция не укладывается в CRUD (Create, Read, Update, Delete) и HTTP-методы. Например, "отправить письмо", "сбросить пароль". Приходится использовать "действия" в виде POST на вложенный ресурс: POST /api/users/123/reset-password.

  5. Проблемы с производительностью при вложенных данных: Чтобы избежать under-fetching, API может начать возвращать глубоко вложенные объекты, но это приводит к over-fetching и сложностям в поддержке. Выбор между этими крайностями — постоянный компромисс.

В моих проектах на Node.js с Express для борьбы с over-fetching я часто реализовывал поддержку query-параметров fields или select.

// Эндпоинт с поддержкой выбора полей
app.get('/api/users/:id', async (req, res) => {
  const fields = req.query.fields ? req.query.fields.split(',') : [];
  const projection = {};
  if (fields.includes('name')) projection.name = 1;
  if (fields.includes('email')) projection.email = 1;
  // ...
  const user = await User.findById(req.params.id, projection);
  res.json(user);
});
// Запрос: GET /api/users/123?fields=name,email

Ответ 18+ 🔞

А, слушай, вот это тема! Я тут на Node.js REST API пилил до овердохуища, и знаешь, какие грабли постоянно по лбу бьют? Сейчас расскажу, как есть, без прикрас.

Ну первое, что бесит — это когда клиент либо получает нихуя не то, что нужно, либо, наоборот, ему прилетает всё подряд, включая твои носки. Это называется over-fetching и under-fetching, если по-умному.

Over-fetching — это когда ты запрашиваешь у /api/users/123 просто имя и аватарку, а тебе в ответ прилетает вся его родословная, включая девичью фамилию бабушки и размер обуви. Сервер, блядь, тратит ресурсы, сеть грузит, а клиенту эта хуйня не нужна, он её просто выкидывает. Ёпта, как будто ты в магазин за хлебом зашёл, а тебе в придачу дают утюг и три кило гречки — ну нахуя?

А under-fetching — это обратная, не менее пиздатая история. Клиенту, чтобы страничку собрать, нужно сделать кучу запросов. Сначала за юзером сходить, потом за его постами, потом за комментами к каждому посту. Получается такая ёбаная карусель из запросов, где каждый ждёт ответа от предыдущего. Производительность, понятное дело, летит в пизду. В Node.js с этим можно бороться, конечно. Либо через хитрожопые query-параметры, где клиент сам указывает, какие поля ему нужны, либо вообще нахуй переходить на GraphQL, если совсем припёрло. Но это уже другая, не менее весёлая песня.

Второй косяк — это отсутствие нормального реального времени. REST, он такой, одноразовый. Запрос-ответ и всё, свободен. А как сделать чат, или чтобы уведомления сами прилетали, без твоего спроса? А нихуя! Приходится городить огород с WebSockets (Socket.IO, ws) или с Server-Sent Events. Это уже отдельная архитектура, со своими подводными камнями. Не то чтобы прям пиздец, но мозг выносит конкретно.

Дальше — версионирование. Вот сделал ты API, всё работает. Потом решил поле userName переименовать в username. И всё, ёперный театр! Все старые клиенты, которые на userName завязаны, ломаются. Приходится плодить эндпоинты: /api/v1/users, /api/v2/users. Код превращается в свалку, где половина — это легаси, которое только поддерживать и поддерживать. Удивление пиздец, как быстро это происходит.

Ещё одна засада — когда нужно сделать какое-то действие, которое в стандартные CRUD-операции не лезет. Ну, типа, «отправить письмо» или «сбросить пароль». По канонам REST, приходится выебываться и делать POST на какой-нибудь кривой эндпоинт вроде POST /api/users/123/reset-password. Выглядит как манда с ушами, но что поделать. Иногда проще нарушить догму и сделать просто POST /api/reset-password с айдишником в теле, но тогда тебе любой пурист скажет, что ты полупидор.

И наконец, главная головная боль — производительность с вложенными данными. Чтобы клиенту не бегать десять раз (under-fetching), хочется отдать ему всё и сразу: юзера, его посты, комментарии к постам, лайки к комментам. Но тогда получается over-fetching, да ещё и запрос в базу становится таким монстром, что он выполняется полчаса. Вечный выбор между хуём и пальмой. Либо клиент долго ждёт, делая кучу запросов, либо сервер долго думает, собирая мега-объект.

Я, например, в своих проектах на Express часто делал такую штуку от over-fetching — добавлял параметр fields. Клиент сам говорит, что ему нужно. Вот смотри, простой пример кода, я его не трогаю, как ты просил:

// Эндпоинт с поддержкой выбора полей
app.get('/api/users/:id', async (req, res) => {
  const fields = req.query.fields ? req.query.fields.split(',') : [];
  const projection = {};
  if (fields.includes('name')) projection.name = 1;
  if (fields.includes('email')) projection.email = 1;
  // ...
  const user = await User.findById(req.params.id, projection);
  res.json(user);
});
// Запрос: GET /api/users/123?fields=name,email

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