1. Планирование мини-проекта
Когда вы только начинаете учить Node.js и Express, всё кажется простым: один файл, пара маршрутов, пару строчек логики — и готово. Но как только проект становится чуть сложнее, код начинает разрастаться, и вы внезапно обнаруживаете, что у вас в файле app.js уже 400 строк, а найти нужную функцию — как искать иголку в стоге сена.
Правильная структура проекта — это как аккуратно разложенные инструменты в ящике: всё на своих местах, каждый элемент отвечает за свою задачу, и любой новый разработчик быстро разберётся, что где лежит.
Давайте соберём мини-проект — REST API для списка задач (todo-list). Это классика: проект достаточно простой, чтобы не утонуть в деталях, но достаточно интересный, чтобы отработать все основные паттерны архитектуры Express.
Что должно уметь наше приложение:
- Получать список задач (GET /tasks)
- Добавлять новую задачу (POST /tasks)
- Получать задачу по id (GET /tasks/:id)
- Обновлять задачу (PUT /tasks/:id)
- Удалять задачу (DELETE /tasks/:id)
Для простоты все задачи будем хранить в памяти (в массиве), чтобы не отвлекаться на базы данных.
2. Основные папки и файлы
Вот типовая структура Express-проекта для нашего мини-приложения:
my-todo-app/
├── app.js
├── package.json
├── routes/
│ └── tasks.js
├── controllers/
│ └── tasksController.js
├── services/
│ └── tasksService.js
├── models/
│ └── task.js
├── utils/
│ └── idGenerator.js
└── README.md
Что где лежит:
- app.js — точка входа, инициализация Express, подключение middleware и маршрутов.
- routes/ — файлы маршрутов (роутеры), которые принимают запросы и передают их контроллерам.
- controllers/ — "дирижёры", которые принимают запрос от роутера, вызывают нужные сервисы и формируют ответ.
- services/ — бизнес-логика, работа с данными (например, массивом задач).
- models/ — описание структуры данных (например, класс Task).
- utils/ — вспомогательные функции (например, генератор уникальных id).
- README.md — краткое описание проекта.
3. Разбираем структуру по кусочкам
app.js — точка входа
Здесь мы создаём приложение Express, подключаем middleware, маршруты и запускаем сервер.
// app.js
const express = require('express');
const tasksRouter = require('./routes/tasks');
const app = express();
const PORT = 3000;
// Middleware для парсинга JSON
app.use(express.json());
// Подключаем роуты
app.use('/tasks', tasksRouter);
// Обработка несуществующих маршрутов
app.use((req, res) => {
res.status(404).json({ error: 'Not found' });
});
// Запуск сервера
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Комментарий:
Всё максимально просто и прозрачно — никакой логики внутри app.js, только настройка и запуск.
routes/tasks.js — маршруты задач
Роутер принимает HTTP-запросы и вызывает соответствующие методы контроллера.
// routes/tasks.js
const express = require('express');
const router = express.Router();
const tasksController = require('../controllers/tasksController');
// Получить список всех задач
router.get('/', tasksController.getAllTasks);
// Добавить новую задачу
router.post('/', tasksController.createTask);
// Получить задачу по id
router.get('/:id', tasksController.getTaskById);
// Обновить задачу по id
router.put('/:id', tasksController.updateTask);
// Удалить задачу по id
router.delete('/:id', tasksController.deleteTask);
module.exports = router;
Комментарий:
Роутер ничего не знает о внутренней логике — он просто делегирует задачи контроллеру.
controllers/tasksController.js — логика обработки запросов
Контроллеры принимают запрос, вызывают сервисы, формируют ответ.
// controllers/tasksController.js
const tasksService = require('../services/tasksService');
// Получить все задачи
exports.getAllTasks = (req, res) => {
const tasks = tasksService.getAll();
res.json(tasks);
};
// Добавить задачу
exports.createTask = (req, res) => {
const { title } = req.body;
if (!title) {
return res.status(400).json({ error: 'Title is required' });
}
const newTask = tasksService.create(title);
res.status(201).json(newTask);
};
// Получить задачу по id
exports.getTaskById = (req, res) => {
const id = req.params.id;
const task = tasksService.getById(id);
if (!task) {
return res.status(404).json({ error: 'Task not found' });
}
res.json(task);
};
// Обновить задачу
exports.updateTask = (req, res) => {
const id = req.params.id;
const { title, completed } = req.body;
const updatedTask = tasksService.update(id, { title, completed });
if (!updatedTask) {
return res.status(404).json({ error: 'Task not found' });
}
res.json(updatedTask);
};
// Удалить задачу
exports.deleteTask = (req, res) => {
const id = req.params.id;
const deleted = tasksService.remove(id);
if (!deleted) {
return res.status(404).json({ error: 'Task not found' });
}
res.json({ message: 'Task deleted' });
};
Комментарий:
Контроллер не занимается хранением или обработкой данных — только принимает запрос, вызывает сервис и возвращает ответ.
services/tasksService.js — бизнес-логика
Здесь вся работа с данными: хранение, поиск, изменение задач.
// services/tasksService.js
const Task = require('../models/task');
const { generateId } = require('../utils/idGenerator');
let tasks = []; // Здесь живёт наш список задач (в памяти, без БД)
// Получить все задачи
exports.getAll = () => tasks;
// Получить задачу по id
exports.getById = (id) => tasks.find(task => task.id === id);
// Создать новую задачу
exports.create = (title) => {
const newTask = new Task(generateId(), title, false);
tasks.push(newTask);
return newTask;
};
// Обновить задачу
exports.update = (id, { title, completed }) => {
const task = tasks.find(t => t.id === id);
if (!task) return null;
if (title !== undefined) task.title = title;
if (completed !== undefined) task.completed = completed;
return task;
};
// Удалить задачу
exports.remove = (id) => {
const index = tasks.findIndex(t => t.id === id);
if (index === -1) return false;
tasks.splice(index, 1);
return true;
};
Комментарий:
Здесь можно было бы подключить базу данных, но для мини-проекта достаточно массива.
models/task.js — описание задачи
Модель — просто класс, описывающий структуру объекта.
// models/task.js
class Task {
constructor(id, title, completed = false) {
this.id = id;
this.title = title;
this.completed = completed;
}
}
module.exports = Task;
Комментарий:
Такой подход пригодится, если вы захотите добавить дополнительные поля (например, дату создания).
utils/idGenerator.js — генератор уникальных id
Вспомогательная функция для генерации id (чтобы не было задач с одинаковыми id).
// utils/idGenerator.js
let currentId = 1;
function generateId() {
return (currentId++).toString();
}
module.exports = { generateId };
Комментарий:
В реальных проектах используют UUID или базы данных, но для мини-проекта и такой вариант сойдёт.
4. Как всё это работает вместе?
Вот схема (блок-схема — почти UML, но без угрозы вашему психическому здоровью):
[Клиент (Postman/браузер)]
|
v
[Маршрут /tasks/:id] <--- routes/tasks.js
|
v
[Контроллер tasksController.js]
|
v
[Сервис tasksService.js]
|
v
[Массив задач/Task]
- Клиент отправляет запрос на сервер.
- Роутер определяет, какой контроллер должен обработать запрос.
- Контроллер вызывает соответствующий метод сервиса.
- Сервис работает с моделями и возвращает результат контроллеру.
- Контроллер отправляет ответ клиенту.
5. Как запускать и тестировать мини-проект
- Создайте папки и файлы согласно структуре выше.
- Инициализируйте проект:
npm init -y npm install express - Запустите сервер:
node app.js - Используйте Postman, curl или браузер для тестирования API:
- GET http://localhost:3000/tasks — получить все задачи
- POST http://localhost:3000/tasks с телом { "title": "Купить хлеб" } — добавить задачу
- GET http://localhost:3000/tasks/1 — получить задачу по id
- PUT http://localhost:3000/tasks/1 с телом { "completed": true } — отметить задачу как выполненную
- DELETE http://localhost:3000/tasks/1 — удалить задачу
Как расширять такой проект?
- Добавьте авторизацию (например, через middleware).
- Подключите базу данных (MongoDB, PostgreSQL).
- Реализуйте валидацию данных (например, через библиотеку joi или zod).
- Добавьте обработку ошибок через отдельный middleware.
- Организуйте тесты (например, с помощью Jest или Mocha).
- Разделите сервисы по разным сущностям, если задач станет больше.
6. Типичные ошибки при структурировании Express-проекта
Ошибка №1: Вся логика в одном файле.
Это соблазнительно для маленьких проектов, но очень быстро приводит к хаосу. Даже если приложение маленькое — старайтесь сразу разделять код на модули.
Ошибка №2: Роутеры содержат бизнес-логику.
Роутер — это диспетчер, он не должен заниматься обработкой данных. Если вся логика в роутере, потом будет сложно её тестировать и переиспользовать.
Ошибка №3: Контроллеры делают всё подряд.
Контроллер — не сервис. Его задача — принять запрос, вызвать сервис, вернуть ответ. Если контроллер начинает работать с массивом задач напрямую — это тревожный звоночек.
Ошибка №4: Нет моделей данных.
Даже если вы не используете базу данных, лучше описывать структуру данных явно (например, через класс Task). Это повысит читаемость и упростит переход к реальной БД.
Ошибка №5: Нет утилит/повторяется код.
Если вы пишете одну и ту же функцию (например, генератор id) в нескольких файлах — пора вынести её в utils.
Ошибка №6: Необработанные ошибки.
Если не обрабатывать ошибки (например, несуществующий id), приложение будет вести себя непредсказуемо. Лучше возвращать информативные коды и сообщения.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ