Express / 37 минут

REST и CRUD-ресурсы

Согласуйте методы HTTP, статусы, идентификаторы и жизненный цикл REST-ресурса.

В процессе Теория: впереди Практика: впереди Тест: впереди
07 / 12
После этой главы
  • Согласуйте методы HTTP, статусы, идентификаторы и жизненный цикл REST-ресурса.
  • Применить тему в задаче «REST и CRUD-ресурсы» и объяснить граничные случаи.

Около 7 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.

Ресурс и его представление

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

CRUD обозначает создание, чтение, изменение и удаление. Эти действия часто выражают через POST, GET, PATCH или PUT и DELETE, но механическое переименование функций ещё не создаёт согласованный API. Нужно определить статусы, допустимые входы, ошибки отсутствия и права на конкретный ресурс. Также важно, какой эффект имеет повтор запроса.

Создание и чтение

POST коллекции обычно создаёт новый ресурс и может отвечать 201 с публичным представлением и Location. GET конкретного идентификатора возвращает ресурс либо согласованный 404. Ошибочный ввод должен быть отвергнут до записи. Если сохранить объект, а потом вернуть клиенту 400, система окажется в неожиданном состоянии: пользователь может повторить действие и получить дубликат.

JavaScript
const notes = new Map();
app.post('/notes', (req, res) => {
  if (typeof req.body?.title !== 'string' || !req.body.title.trim()) {
    return res.status(400).json({ error: 'title' });
  }
  const id = crypto.randomUUID();
  const note = { id, title: req.body.title.trim() };
  notes.set(id, note);
  return res.status(201).location(`/notes/${id}`).json(note);
});

Для этого фрагмента crypto должен быть получен из подходящего API Node.js. Map здесь — учебное хранилище процесса. Оно не сохраняет данные после перезапуска и не синхронизируется между несколькими экземплярами сервера. В готовом приложении адаптер хранения и источник уникальности выбирают под реальные требования.

Обновление и разрешённые поля

PATCH часто описывает частичное изменение, PUT — замену представления с согласованной семантикой. Не принимайте любой объект как допустимое обновление. Например, пользователь может менять title, но не ownerId или служебную роль. Поля нужно явно выбрать и проверить, а отсутствие отличить от намеренного сброса, если контракт поддерживает null.

JavaScript
app.patch('/notes/:id', (req, res) => {
  const note = notes.get(req.params.id);
  if (!note) return res.sendStatus(404);
  if (typeof req.body?.title !== 'string' || !req.body.title.trim()) {
    return res.sendStatus(400);
  }
  const updated = { ...note, title: req.body.title.trim() };
  notes.set(note.id, updated);
  res.json(updated);
});

В реальном защищённом API перед обновлением также нужна проверка владельца или другого права. Успешная валидация title не заменяет авторизацию. Конкурентные изменения могут потребовать версии записи или другого механизма предотвращения потерянного обновления: два клиента не обязаны знать, что работают со старой копией.

Удаление и идемпотентность

DELETE должен иметь понятный эффект для уже отсутствующего ресурса. Допустимый выбор статуса зависит от принятого контракта, но повтор действия не должен неожиданно удалить другой объект. Идемпотентность относится к целевому эффекту, а не к обязательному побайтовому равенству каждого ответа, заголовка даты или журнала.

JavaScript
app.delete('/notes/:id', (req, res) => {
  notes.delete(req.params.id);
  res.sendStatus(204);
});

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

Ошибки, транзакции и границы сценария

Ресурсные операции часто затрагивают несколько записей: заметку, событие аудита, счётчик или связь. Если они должны согласованно сохраниться, сценарий использует транзакцию или другую обоснованную модель доставки. Разрозненные await не делают последовательность атомарной. Сбой между двумя записями нужно учитывать заранее.

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

Практика жизненного цикла заметки

В задании POST создаёт заметку с учебным id='1', а следующий GET проверяет, появилась ли она. Для неверного title должны получиться 400 и затем 404: ошибочная операция не создаёт ресурс. У каждого solve новая Map, поэтому разные тесты не используют остатки состояния соседнего запуска.

Проверьте обычную строку, внешние пробелы, пустое после trim значение и null. Затем расширьте проект обновлением и удалением, сохранив явный allowlist полей. Добавьте тест повторного GET, отсутствующего id и запрета доступа чужому владельцу. Именно связанные сценарии показывают, что CRUD — это модель поведения, а не только набор красивых URL.

Загружаем редактор…
Следующая темаОбработка ошибок Express

Музыка: Reverie — Scott Buckley · CC BY 4.0