Express / 37 минут
Слои и зависимости API
Отделите HTTP, сценарий и хранилище; передавайте зависимости явно.
- Отделите HTTP, сценарий и хранилище; передавайте зависимости явно.
- Применить тему в задаче «Слои и зависимости API» и объяснить граничные случаи.
Около 7 минут на чтение и разбор примеров. Практика — отдельно, в своём темпе.
Причины разделять обязанности
Небольшой handler легко написать в одном файле. Со временем он начинает читать параметры, проверять пользователя, обращаться к БД, отправлять письмо и формировать ответ. Изменение одной детали тогда затрагивает несвязанные правила. Слои нужны не ради количества директорий, а чтобы причины изменения и зависимости оставались понятными.
У HTTP-адаптера своя ответственность: принять проверенный ввод, вызвать сценарий и представить результат как статус и публичный DTO. Сервис описывает сценарий приложения, а репозиторий или другой адаптер занимается конкретным способом хранения. Это не требует микросервисов: все части могут жить в одном процессе и одном небольшом проекте.
Тонкий HTTP-слой
Контроллер не должен знать внутренние детали SQL или создавать новое подключение к БД на каждый запрос. Он также не должен возвращать сырую запись пользователя только потому, что она уже есть в памяти. Явный публичный ответ удерживает контракт стабильным и предотвращает утечку приватных полей.
function createUsersRouter(service) {
const router = express.Router();
router.get('/:id', async (req, res) => {
const user = await service.findPublic(req.params.id);
if (!user) return res.status(404).json({ error: 'missing' });
res.json(user);
});
return router;
}Здесь HTTP-слой знает, как из результата сценария сделать ответ, но не знает, где именно живут записи. Сам service передаётся фабрике извне. Это делает настройку приложения явной и позволяет тесту предоставить контролируемую зависимость без переписывания production-кода.
Сценарий и репозиторий
Сервис принимает данные и зависимости предметного действия. Он может проверить доменное правило, запросить репозиторий и построить публичную проекцию. Репозиторий предоставляет узкий набор операций хранения. Не обязательно создавать универсальный generic CRUD на все таблицы: несколько понятных методов часто лучше отражают реальную задачу.
function createUsersService(repository) {
return {
async findPublic(id) {
const user = await repository.find(id);
return user ? { id: user.id, name: user.name } : null;
},
};
}Сервис не получает res и не выбирает CSS-состояние кнопки. Поэтому один и тот же сценарий можно проверить без HTTP или вызвать из другого доверенного адаптера. При этом не стоит выносить каждое простое выражение в отдельный сервис: граница должна иметь понятную ответственность и полезный контракт.
Точка сборки и время жизни
Зависимости связывают в одном понятном месте: фабрике приложения или bootstrap-модуле. Там создают конфигурацию, клиент БД, репозитории, сервисы и routers. Общий пул БД может жить долго, а контекст подтверждённого пользователя относится к запросу. Не смешивайте эти сроки жизни в глобальной переменной «currentUser».
function createApp(repository) {
const app = express();
const service = createUsersService(repository);
app.use('/users', createUsersRouter(service));
return app;
}Такой dependency injection не требует большого контейнера. Это обычная передача зависимостей, которая позже станет основой понимания NestJS. Контейнер автоматизирует создание и разрешение графа, но не решает за разработчика, как правильно разделить бизнес-возможности.
Ошибки и транзакционные границы
Ожидаемое отсутствие записи может быть результатом сценария, а неожиданный отказ адаптера — исключением. Не позволяйте HTTP-статусам просачиваться во все низкоуровневые утилиты без необходимости. При нескольких связанных записях именно сценарий должен понимать их атомарность и вызывать подходящую транзакционную операцию адаптера.
Тестовый in-memory репозиторий не доказывает все свойства настоящей БД: уникальность, блокировки и особенности транзакций проверяются отдельно на реальной инфраструктуре. Подмена полезна для логики сценария, но не должна создавать ложную уверенность в поведении хранилища. Разные тесты покрывают разные границы.
Практика внедрения поиска
В упражнении создаётся сервис над переданным массивом и маршрут GET /users/:id. Ответ содержит только id и name. Дополнительный secret во входных данных не должен появиться в JSON. Пустой список или неизвестный id дают согласованный 404. Это маленькая проверка одновременно архитектурной границы и публичного представления.
После решения замените массив на другой репозиторий, не меняя код router. Затем протестируйте сервис напрямую и отдельно проверьте HTTP-ответ. Для итогового API заметок предложите структуру по возможностям: notes, auth и общая инфраструктура. Она должна помогать найти сценарий, а не требовать обхода десятка абстрактных слоёв для одного понятного действия.