1. Введение
CRUD — это аббревиатура, которую любят все бэкендеры (и даже фронтендеры иногда делают вид, что знают, что это такое):
- Create — создать (добавить новый элемент)
- Read — прочитать (получить элемент или список)
- Update — обновить (изменить существующий элемент)
- Delete — удалить (стереть элемент из памяти)
Такая четверка операций встречается в каждом уважающем себя API, ведь именно так обычно работают с данными: что-то добавляем, что-то читаем, что-то меняем, что-то удаляем. Даже если вы — суперхакер, который пишет бота для Telegram, или просто делаете ToDo-лист для домашних заданий.
В этой лекции мы реализуем все эти операции для массива задач в памяти — без базы данных, только JS и Express. Это классика жанра, и на собеседованиях любят спрашивать: "А как бы вы реализовали CRUD?" — теперь вы будете знать ответ!
Создаём сервер Express и массив данных
Давайте начнем с самого простого — создадим сервер и массив, который будет хранить наши задачи (tasks). Каждая задача будет объектом с полями id, title и completed.
// app.js
const express = require('express');
const app = express();
const PORT = 3000;
// Middleware для парсинга JSON из тела запроса
app.use(express.json());
// Наш "бэкенд" — массив задач в памяти
let tasks = [
{ id: 1, title: "Купить хлеб", completed: false },
{ id: 2, title: "Сделать домашку по Node.js", completed: false }
];
// Для генерации уникальных id (очень простая стратегия)
let nextId = 3;
Примечание: В реальной жизни для хранения данных используют базы данных (MongoDB, Postgres, даже Excel-файлы — не спрашивайте…). Но для обучения и прототипирования массив — отличный вариант.
2. Create (POST): Добавление новой задачи
Чтобы добавить задачу, клиент отправляет POST-запрос на /tasks с данными задачи в теле запроса (например, только title). Мы должны создать задачу, добавить в массив и вернуть её клиенту.
// Создание новой задачи
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title) {
// Если не передан title — ошибка!
return res.status(400).json({ error: "Поле 'title' обязательно" });
}
const newTask = {
id: nextId++, // уникальный id
title,
completed: false // новая задача всегда не выполнена
};
tasks.push(newTask);
res.status(201).json(newTask); // 201 Created
});
- Мы деструктурируем title из req.body.
- Проверяем, что title есть — иначе возвращаем ошибку 400 (Bad Request).
- Создаём задачу, присваиваем ей уникальный id, пушим в массив.
- Отправляем клиенту созданную задачу и статус 201.
3. Read (GET): Получение списка и одной задачи
Получить все задачи
// Получить все задачи
app.get('/tasks', (req, res) => {
res.json(tasks);
});
Здесь всё просто — отправляем весь массив. В реальных API часто добавляют пагинацию, фильтрацию, сортировку, но пока оставим всё по-честному.
Получить одну задачу по id
// Получить задачу по id
app.get('/tasks/:id', (req, res) => {
const id = Number(req.params.id); // id из строки превращаем в число
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: "Задача не найдена" });
}
res.json(task);
});
- Используем параметр маршрута :id (например, /tasks/2).
- Преобразуем id в число (иначе можно попасть в ловушку сравнения строк и чисел).
- Ищем задачу по id, если не нашли — возвращаем 404.
4. Update (PUT/PATCH): Редактирование задачи
В классическом REST API для обновления используют либо PUT (заменить всю задачу), либо PATCH (изменить только часть). Мы реализуем оба варианта, чтобы почувствовать разницу.
PUT: Полная замена задачи
// Полная замена задачи (PUT)
app.put('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const { title, completed } = req.body;
// Проверим, что оба поля есть
if (typeof title !== 'string' || typeof completed !== 'boolean') {
return res.status(400).json({ error: "title (string) и completed (boolean) обязательны" });
}
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: "Задача не найдена" });
}
// Обновляем поля задачи
task.title = title;
task.completed = completed;
res.json(task);
});
PATCH: Частичное обновление
// Частичное обновление задачи (PATCH)
app.patch('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: "Задача не найдена" });
}
// Обновляем только те поля, которые пришли
if ('title' in req.body) {
if (typeof req.body.title !== 'string') {
return res.status(400).json({ error: "title должен быть строкой" });
}
task.title = req.body.title;
}
if ('completed' in req.body) {
if (typeof req.body.completed !== 'boolean') {
return res.status(400).json({ error: "completed должен быть boolean" });
}
task.completed = req.body.completed;
}
res.json(task);
});
В чём разница?
PUT требует все поля и заменяет задачу полностью.
PATCH меняет только те поля, которые пришли (можно поменять только completed, не трогая title).
5. Delete (DELETE): Удаление задачи
Удалять — не строить, но иногда без этого никак!
// Удалить задачу по id
app.delete('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const index = tasks.findIndex(t => t.id === id);
if (index === -1) {
return res.status(404).json({ error: "Задача не найдена" });
}
// Удаляем задачу из массива
const deleted = tasks.splice(index, 1)[0];
res.json(deleted); // возвращаем удалённую задачу
});
- Находим индекс задачи по id.
- Если не нашли — 404.
- Удаляем с помощью splice, возвращаем удалённую задачу клиенту.
6. Итоговый код приложения
Соберём всё вместе — получится мини-API для задач:
const express = require('express');
const app = express();
const PORT = 3000;
app.use(express.json());
let tasks = [
{ id: 1, title: "Купить хлеб", completed: false },
{ id: 2, title: "Сделать домашку по Node.js", completed: false }
];
let nextId = 3;
// CREATE
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: "Поле 'title' обязательно" });
}
const newTask = { id: nextId++, title, completed: false };
tasks.push(newTask);
res.status(201).json(newTask);
});
// READ (all)
app.get('/tasks', (req, res) => {
res.json(tasks);
});
// READ (one)
app.get('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: "Задача не найдена" });
}
res.json(task);
});
// UPDATE (PUT)
app.put('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const { title, completed } = req.body;
if (typeof title !== 'string' || typeof completed !== 'boolean') {
return res.status(400).json({ error: "title (string) и completed (boolean) обязательны" });
}
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: "Задача не найдена" });
}
task.title = title;
task.completed = completed;
res.json(task);
});
// UPDATE (PATCH)
app.patch('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: "Задача не найдена" });
}
if ('title' in req.body) {
if (typeof req.body.title !== 'string') {
return res.status(400).json({ error: "title должен быть строкой" });
}
task.title = req.body.title;
}
if ('completed' in req.body) {
if (typeof req.body.completed !== 'boolean') {
return res.status(400).json({ error: "completed должен быть boolean" });
}
task.completed = req.body.completed;
}
res.json(task);
});
// DELETE
app.delete('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const index = tasks.findIndex(t => t.id === id);
if (index === -1) {
return res.status(404).json({ error: "Задача не найдена" });
}
const deleted = tasks.splice(index, 1)[0];
res.json(deleted);
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
7. Как это тестировать? (Практика)
Можно использовать Postman, Insomnia, curl или даже расширение REST Client в VS Code.
Примеры запросов:
- Получить все задачи:
GET http://localhost:3000/tasks - Получить задачу №2:
GET http://localhost:3000/tasks/2 - Добавить задачу:
POST http://localhost:3000/tasks
{ "title": "Погладить кота" } - Обновить задачу полностью:
PUT http://localhost:3000/tasks/1
{ "title": "Купить хлеб и молоко", "completed": true } - Частично обновить задачу:
PATCH http://localhost:3000/tasks/2
{ "completed": true } - Удалить задачу:
DELETE http://localhost:3000/tasks/1
8. Типичные ошибки при реализации CRUD в памяти
Ошибка №1: забыли парсить JSON.
Если не подключить express.json(), то req.body будет undefined, и сервер будет ругаться при попытке получить поля из тела запроса.
Ошибка №2: не проверяете типы данных.
Если не проверять, что title — строка, а completed — boolean, можно получить очень странные баги. Например, задача с completed: "true" (строкой, а не булевым значением).
Ошибка №3: не обрабатываете ситуацию, когда задача не найдена.
Если не возвращать 404, а просто ничего не делать, клиент будет в замешательстве: "Я что-то удалил или нет?"
Ошибка №4: id не уникальны.
Если не увеличивать nextId, можно получить две задачи с одинаковым id — и тогда начнётся веселье, но не для пользователя.
Ошибка №5: забыли преобразовать id в число.
Параметры маршрута (req.params.id) — это строки! Если сравнивать их с числовыми id, поиск не сработает.
Ошибка №6: мутируете массив напрямую без копирования.
В нашем случае это не страшно (мы работаем в памяти), но в реальных приложениях стоит аккуратно относиться к мутациям данных.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ