1. Что такое REST API?
Перед тем как погрузиться в код, давайте коротко вспомним, что вообще такое REST API. REST (Representational State Transfer) — это стиль архитектуры, при котором взаимодействие с сервером происходит через стандартные HTTP-методы (GET, POST, PUT, DELETE и т.д.), а данные передаются обычно в формате JSON. Каждый endpoint (маршрут) отвечает за определённое действие с ресурсом (например, задачей).
В нашем случае ресурс — это "задача" (task). Мы хотим реализовать API, который позволит:
- Получить список всех задач
- Получить одну задачу по id
- Добавить новую задачу
- Обновить существующую задачу
- Удалить задачу
Если сравнивать с жизнью — представьте, что вы приходите в библиотеку (сервер), и по разным адресам (endpoint) можете получить список всех книг, взять одну, добавить новую, обновить или выбросить старую (только, пожалуйста, не делайте этого с настоящими книгами).
Минимальная структура Express-приложения
Для начала вспомним, как выглядит минимальное Express-приложение. Мы будем развивать именно это приложение по мере изучения темы:
const express = require('express');
const app = express();
const PORT = 3000;
// Позволяет Express читать JSON из тела запроса
app.use(express.json());
app.listen(PORT, () => {
console.log(`Сервер запущен на http://localhost:${PORT}`);
});
Всё, что нам нужно — это Express и немного энтузиазма (и, возможно, кофе).
Где будем хранить задачи?
Для простоты мы пока не будем использовать базы данных. Все задачи будут храниться в массиве в памяти. Это не очень надёжно (при перезапуске сервера всё исчезнет), но для тренировки и понимания REST этого достаточно.
let tasks = [
// Пример задачи
// { id: 1, title: "Купить хлеб", completed: false }
];
Каждая задача — это объект с полями:
- id — уникальный идентификатор (целое число)
- title — текст задачи
- completed — выполнена ли задача (boolean)
Таблица соответствия методов и маршрутов
| Действие | HTTP-метод | Маршрут | Описание |
|---|---|---|---|
| Получить все задачи | |
|
Вернуть список задач |
| Получить задачу | |
|
Вернуть одну задачу |
| Добавить задачу | |
|
Добавить новую задачу |
| Обновить задачу | |
|
Изменить задачу |
| Удалить задачу | |
|
Удалить задачу |
2. Реализация CRUD-операций
Получить список всех задач (GET /tasks)
app.get('/tasks', (req, res) => {
res.json(tasks);
});
Это очень просто: сервер возвращает весь массив задач в формате JSON.
Получить задачу по id (GET /tasks/:id)
app.get('/tasks/:id', (req, res) => {
const id = Number(req.params.id); // id приходит в виде строки!
const task = tasks.find(t => t.id === id);
if (!task) {
// Если задачи с таким id нет — возвращаем 404
return res.status(404).json({ error: 'Задача не найдена' });
}
res.json(task);
});
Обратите внимание: всегда проверяйте, что задача найдена. Иначе клиент получит "undefined" и будет грустить.
Добавить новую задачу (POST /tasks)
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title || typeof title !== 'string') {
// Проверяем, что title есть и это строка
return res.status(400).json({ error: 'Некорректный заголовок задачи' });
}
// Генерируем новый id (берём максимальный из существующих и +1)
const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1;
const newTask = {
id: newId,
title,
completed: false
};
tasks.push(newTask);
res.status(201).json(newTask); // 201 — Created
});
Здесь мы принимаем данные из тела запроса. Не забываем про валидацию — иначе можно получить задачи с пустым названием или вообще без него.
Обновить задачу (PUT /tasks/:id)
app.put('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const { title, completed } = req.body;
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
// Обновляем только те поля, которые пришли
if (title !== undefined) {
if (typeof title !== 'string') {
return res.status(400).json({ error: 'Некорректный заголовок задачи' });
}
task.title = title;
}
if (completed !== undefined) {
if (typeof completed !== 'boolean') {
return res.status(400).json({ error: 'Некорректный completed (ожидается true/false)' });
}
task.completed = completed;
}
res.json(task);
});
PUT обычно означает "заменить всю задачу", но на практике часто разрешают частичное обновление (иначе обновлять только статус задачи было бы неудобно). Можно реализовать и метод PATCH, но для простоты пока используем PUT.
Удалить задачу (DELETE /tasks/: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);
});
Метод DELETE удаляет задачу и возвращает удалённый объект (или можно возвращать 204 No Content, если не хотите ничего отправлять в ответе).
3. Итоговый код приложения
Давайте объединим всё, что написали выше, в одно приложение. Это поможет не только закрепить материал, но и позволит вам скопировать и сразу попробовать код у себя.
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: "Сделать домашку по JS", completed: false },
{ id: 3, title: "Позвонить бабушке", completed: true }
];
// Получить все задачи
app.get('/tasks', (req, res) => {
res.json(tasks);
});
// Получить одну задачу по id
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);
});
// Добавить новую задачу
app.post('/tasks', (req, res) => {
const { title } = req.body;
if (!title || typeof title !== 'string') {
return res.status(400).json({ error: 'Некорректный заголовок задачи' });
}
const newId = tasks.length > 0 ? Math.max(...tasks.map(t => t.id)) + 1 : 1;
const newTask = { id: newId, title, completed: false };
tasks.push(newTask);
res.status(201).json(newTask);
});
// Обновить задачу
app.put('/tasks/:id', (req, res) => {
const id = Number(req.params.id);
const { title, completed } = req.body;
const task = tasks.find(t => t.id === id);
if (!task) {
return res.status(404).json({ error: 'Задача не найдена' });
}
if (title !== undefined) {
if (typeof title !== 'string') {
return res.status(400).json({ error: 'Некорректный заголовок задачи' });
}
task.title = title;
}
if (completed !== undefined) {
if (typeof completed !== 'boolean') {
return res.status(400).json({ error: 'Некорректный completed (ожидается true/false)' });
}
task.completed = completed;
}
res.json(task);
});
// Удалить задачу
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(`Сервер запущен на http://localhost:${PORT}`);
});
4. Как тестировать API? (Postman, curl, VS Code REST Client)
Чтобы проверить работу вашего API, можно использовать:
- Postman — удобный инструмент с графическим интерфейсом.
- curl — команда для терминала.
- Встроенные плагины для VS Code, например, REST Client.
Примеры запросов
Получить все задачи:
GET http://localhost:3000/tasks
Получить одну задачу:
GET http://localhost:3000/tasks/2
Добавить задачу:
POST http://localhost:3000/tasks
Content-Type: application/json
{
"title": "Погулять с собакой"
}
Обновить задачу:
PUT http://localhost:3000/tasks/1
Content-Type: application/json
{
"completed": true
}
Удалить задачу:
DELETE http://localhost:3000/tasks/3
5. Типичные ошибки при создании REST API
Ошибка 1: Не проверяется существование задачи перед обновлением или удалением.
Если попытаться удалить несуществующую задачу, сервер должен вернуть 404, а не просто молча ничего не делать. Иначе клиент будет думать, что всё прошло успешно, а задача не исчезла — и будет негодовать.
Ошибка 2: Не проводится валидация данных.
Если не проверять, что title — строка, а completed — boolean, в массив задач может попасть что угодно: числа, объекты, пустые строки, и даже котики (но котики — это хорошо, а вот мусорные данные — плохо).
Ошибка 3: Использование одинаковых id для разных задач.
Если при добавлении новой задачи не генерировать уникальный id, можно случайно затереть старую задачу. Генерируйте id на основе максимального текущего.
Ошибка 4: Неправильный Content-Type.
Если клиент отправляет данные без заголовка Content-Type: application/json, req.body может быть пустым. Не забывайте про этот заголовок при отправке запросов.
Ошибка 5: Необработанные ошибки приводят к падению сервера.
Если где-то возникла ошибка (например, опечатка в названии поля), сервер может упасть. Лучше всегда возвращать понятные сообщения об ошибке и не допускать аварийного завершения приложения.
Ошибка 6: Нарушение принципов REST.
Например, использовать GET для удаления или изменения данных — так делать нельзя, иначе REST API превращается в REST in peace.
Ошибка 7: Отсутствие статусов ответа.
Не забывайте возвращать правильные HTTP-статусы (201 для создания, 404 для не найдено, 400 для некорректных данных и т.д.). Это важно для корректной работы клиента.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ