CRUD-маршруты на практике

CRUD: Create, Read, Update, Delete — четыре операции, покрывающие почти любой ресурс.
«Освоив CRUD для одного ресурса, ты умеешь делать API для любого: меняется только название.»

CRUD — это четыре базовые операции над данными. Реализовав их для одного ресурса, ты получаешь шаблон для всех остальных. В этом уроке напишем полный набор маршрутов для ресурса "задачи" с хранением в памяти — без базы, чтобы сфокусироваться на логике API.

Полный CRUD

Соберём все шесть маршрутов вокруг массива в памяти. В реальном приложении массив заменит база, но интерфейс останется тем же:

let tasks = [{ id: 1, title: 'Выучить Express', done: false }];
let nextId = 2;

// READ: список
app.get('/tasks', (req, res) => res.json(tasks));

// READ: один
app.get('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === Number(req.params.id));
  if (!task) return res.status(404).json({ error: 'не найдено' });
  res.json(task);
});

// CREATE
app.post('/tasks', (req, res) => {
  const task = { id: nextId++, title: req.body.title, done: false };
  tasks.push(task);
  res.status(201).json(task);
});

// UPDATE (частично)
app.patch('/tasks/:id', (req, res) => {
  const task = tasks.find(t => t.id === Number(req.params.id));
  if (!task) return res.status(404).json({ error: 'не найдено' });
  Object.assign(task, req.body);
  res.json(task);
});

// DELETE
app.delete('/tasks/:id', (req, res) => {
  tasks = tasks.filter(t => t.id !== Number(req.params.id));
  res.status(204).end();
});

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

CRUD над массивом в браузере

Чтобы прочувствовать логику без сервера, реализуем CRUD над обычным массивом. Это та же бизнес-логика, что внутри обработчиков выше:

let items = [];
let id = 1;

function create(title) {
  const item = { id: id++, title };
  items.push(item);
  return item;
}
function read(itemId) { return items.find(i => i.id === itemId) || null; }
function update(itemId, title) {
  const item = read(itemId);
  if (item) item.title = title;
  return item;
}
function remove(itemId) { items = items.filter(i => i.id !== itemId); }

create('Первая');
create('Вторая');
update(1, 'Обновлённая');
remove(2);
console.log(items); // [{ id: 1, title: 'Обновлённая' }]

Жизненный цикл записи

POST   -> создаётся   -> [ запись существует ]
GET    -> читается    -> [ запись существует ]
PATCH  -> меняется    -> [ запись изменена ]
DELETE -> удаляется   -> [ записи больше нет -> 404 на GET ]

Как работает под капотом

Каждый CRUD-маршрут — это обычный обработчик Express, который читает req.params/req.body, меняет источник данных и отвечает через res.json()/res.status(). Разница между "в памяти" и "в базе" — только в том, чем заменить массив. Поэтому, отладив логику на массиве, ты легко перенесёшь её на реальное хранилище.

Частые ошибки

  • Не проверять наличие. Перед update/delete убедись, что ресурс есть, иначе клиент не отличит успех от "ничего не нашлось".
  • Сравнивать id строкой. req.params.id — строка, элементы массива хранят число. Преобразуй через Number().
  • Возвращать 200 на удаление без тела. Чище отдать 204 No Content.

Best practices

  • Выноси поиск ресурса в отдельную функцию или middleware, чтобы не дублировать.
  • Отвечай согласованными статусами: 201, 200, 204, 404.
  • Не возвращай внутренние поля (пароли, токены) — формируй ответ явно.

Итоги

CRUD — это четыре операции и предсказуемый набор статусов. Логика всех ресурсов однотипна, поэтому её удобно шаблонизировать. Сейчас все маршруты лежат в одном файле; в следующем уроке наведём порядок с помощью express.Router.

Пагинация и форма коллекции

Список ресурсов почти никогда не отдают целиком: представь таблицу в миллион строк. Поэтому коллекции отдают порциями, управляя ими через query: ?page=2&limit=20 или через курсор. Ответ при этом удобно завернуть в объект с метаданными — сколько всего записей, какая страница, есть ли следующая — а не отдавать голый массив. Это даёт клиенту контекст и оставляет место для расширения, не ломая контракт. Привыкай проектировать списки с пагинацией с самого начала: добавить её в работающее API задним числом почти всегда означает ломающее изменение для всех клиентов сразу.

Проверьте себя
1. Какой статус уместен для успешного DELETE без тела ответа?
A200 OK
B204 No Content
C201 Created
D404 Not Found
2. Почему перед PATCH/DELETE стоит проверять наличие ресурса?
AИначе Express упадёт
BЧтобы вернуть 404, если ресурса нет, а не выдать ложный успех
CЭто требование REST по скорости
DИначе req.body будет пустым