Node.jsМодуль 4: HTTP и Express
REST API
Принципы REST и создание API
Цель урока
В этом уроке ты научишься:
- Понимать принципы REST
- Проектировать API endpoints
- Использовать правильные HTTP методы и статус-коды
Принципы REST
REST (Representational State Transfer) — архитектурный стиль для API.
Ключевые принципы
- Клиент-сервер — разделение ответственности
- Stateless — сервер не хранит состояние клиента
- Кэширование — ответы могут кэшироваться
- Единообразный интерфейс — стандартные методы и URL
- Многоуровневая система — клиент не знает о промежуточных серверах
HTTP методы
| Метод | Описание | Идемпотентность |
|---|---|---|
| GET | Получение ресурса | Да |
| POST | Создание ресурса | Нет |
| PUT | Полное обновление | Да |
| PATCH | Частичное обновление | Да |
| DELETE | Удаление | Да |
Проектирование URL
Хорошие практики
GET /api/users # Список пользователей
GET /api/users/123 # Один пользователь
POST /api/users # Создать пользователя
PUT /api/users/123 # Обновить пользователя
DELETE /api/users/123 # Удалить пользователя
GET /api/users/123/posts # Посты пользователя
POST /api/users/123/posts # Создать пост для пользователяПлохие практики
GET /api/getUsers # Глагол в URL
POST /api/createUser # Глагол в URL
GET /api/user # Единственное число
DELETE /api/users/delete/123 # Лишний сегментHTTP статус-коды
2xx — Успех
| Код | Название | Использование |
|---|---|---|
| 200 | OK | GET, PUT, PATCH успешно |
| 201 | Created | POST создал ресурс |
| 204 | No Content | DELETE успешно |
4xx — Ошибка клиента
| Код | Название | Использование |
|---|---|---|
| 400 | Bad Request | Невалидные данные |
| 401 | Unauthorized | Не авторизован |
| 403 | Forbidden | Нет прав доступа |
| 404 | Not Found | Ресурс не найден |
| 409 | Conflict | Конфликт (дубликат) |
| 422 | Unprocessable Entity | Ошибка валидации |
5xx — Ошибка сервера
| Код | Название | Использование |
|---|---|---|
| 500 | Internal Server Error | Неожиданная ошибка |
| 503 | Service Unavailable | Сервис недоступен |
Полный пример API
const express = require('express');
const router = express.Router();
let users = [];
let nextId = 1;
// GET /api/users
router.get('/', (req, res) => {
const { page = 1, limit = 10, sort } = req.query;
let result = [...users];
// Сортировка
if (sort) {
const [field, order] = sort.split(':');
result.sort((a, b) => {
if (order === 'desc') return b[field] > a[field] ? 1 : -1;
return a[field] > b[field] ? 1 : -1;
});
}
// Пагинация
const start = (page - 1) * limit;
const paginated = result.slice(start, start + parseInt(limit));
res.json({
data: paginated,
meta: {
total: users.length,
page: parseInt(page),
limit: parseInt(limit),
pages: Math.ceil(users.length / limit)
}
});
});
// GET /api/users/:id
router.get('/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({ data: user });
});
// POST /api/users
router.post('/', (req, res) => {
const { name, email } = req.body;
// Валидация
if (!name || !email) {
return res.status(400).json({
error: 'Name and email are required'
});
}
// Проверка дубликата
if (users.some(u => u.email === email)) {
return res.status(409).json({
error: 'Email already exists'
});
}
const user = {
id: nextId++,
name,
email,
createdAt: new Date()
};
users.push(user);
res.status(201).json({ data: user });
});
// PUT /api/users/:id
router.put('/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({
error: 'Name and email are required'
});
}
users[index] = {
...users[index],
name,
email,
updatedAt: new Date()
};
res.json({ data: users[index] });
});
// PATCH /api/users/:id
router.patch('/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
Object.assign(user, req.body, { updatedAt: new Date() });
res.json({ data: user });
});
// DELETE /api/users/:id
router.delete('/:id', (req, res) => {
const index = users.findIndex(u => u.id === parseInt(req.params.id));
if (index === -1) {
return res.status(404).json({ error: 'User not found' });
}
users.splice(index, 1);
res.status(204).send();
});
module.exports = router;Формат ответа
Успешный ответ
{
"data": {
"id": 1,
"name": "Иван",
"email": "ivan@example.com"
}
}Список с пагинацией
{
"data": [
{ "id": 1, "name": "Иван" },
{ "id": 2, "name": "Мария" }
],
"meta": {
"total": 100,
"page": 1,
"limit": 10,
"pages": 10
}
}Ошибка
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email is required",
"details": [
{ "field": "email", "message": "Required" }
]
}
}Практика
Задание: API для задач
Задача: Создай REST API для управления задачами.
Решение:
const express = require('express');
const router = express.Router();
let tasks = [];
let nextId = 1;
router.get('/', (req, res) => {
const { status } = req.query;
let result = tasks;
if (status) {
result = tasks.filter(t => t.status === status);
}
res.json({ data: result });
});
router.post('/', (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title required' });
}
const task = {
id: nextId++,
title,
status: 'pending',
createdAt: new Date()
};
tasks.push(task);
res.status(201).json({ data: task });
});
router.patch('/:id/complete', (req, res) => {
const task = tasks.find(t => t.id === parseInt(req.params.id));
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
task.status = 'completed';
task.completedAt = new Date();
res.json({ data: task });
});
module.exports = router;Проверь себя
- Какой HTTP метод для создания ресурса?
- Какой статус-код для успешного DELETE?
- Почему URL должны быть во множественном числе?