JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Реализация CRUD-операций для массива в памяти

Реализация CRUD-операций для массива в памяти

Модуль 4: Node.js, Next.js и Angular
5 уровень , 7 лекция
Открыта

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 — строка, а completedboolean, можно получить очень странные баги. Например, задача с completed: "true" (строкой, а не булевым значением).

Ошибка №3: не обрабатываете ситуацию, когда задача не найдена.
Если не возвращать 404, а просто ничего не делать, клиент будет в замешательстве: "Я что-то удалил или нет?"

Ошибка №4: id не уникальны.
Если не увеличивать nextId, можно получить две задачи с одинаковым id — и тогда начнётся веселье, но не для пользователя.

Ошибка №5: забыли преобразовать id в число.
Параметры маршрута (req.params.id) — это строки! Если сравнивать их с числовыми id, поиск не сработает.

Ошибка №6: мутируете массив напрямую без копирования.
В нашем случае это не страшно (мы работаем в памяти), но в реальных приложениях стоит аккуратно относиться к мутациям данных.

1
Задача
Модуль 4: Node.js, Next.js и Angular, 5 уровень, 7 лекция
Недоступна
Получение списка задач
Получение списка задач
1
Задача
Модуль 4: Node.js, Next.js и Angular, 5 уровень, 7 лекция
Недоступна
Добавление новой задачи
Добавление новой задачи
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ