20 уроков · 20 заданий · 20 проверок понимания
Roadmap backend-разработчика
20 практических шагов: HTTP, Node.js, PostgreSQL, доступ к данным, тесты и выпуск собственного API.
Ваш итоговый проект
Taskboard — API доски задач. Пользователь создаёт проект, добавляет задачи, меняет их статус и видит только свои данные. К концу маршрута сервис переживает перезапуск, проверяется тестами и запускается из контейнера.
Что нужно на старте
Нужны переменные, функции, массивы и объекты JavaScript, базовое понимание async/await. Установите поддерживаемую LTS-версию Node.js, Git и PostgreSQL либо Docker для локальной базы. Первые шаги работают без платных сервисов. Примеры используют JavaScript ESM; TypeScript можно добавить после работающего вертикального сценария.
Как проходить
Идите по номерам: прочитайте объяснение, разберите пример, выполните задание в своём проекте и ответьте на вопрос. Каждые пять шагов завершают очередную часть маршрута. Практику вы проверяете по критериям и подтверждаете самостоятельно; код в Loono не исполняется.
Материалы открыты без регистрации. Прогресс публичной карты сохраняется в этом браузере. Для серверного сохранения скачайте карту и импортируйте её в свой аккаунт через кнопку импорта.
Скачать карту для личного импорта (JSON)Авторский материал Loono. Редакция 2026-09-07. Внешние сервисы могут требовать отдельного доступа; это отмечено в заданиях.
Программа и материалы
1. Первый работающий HTTP-сервис
01. Границы сервисаОписать один законченный пользовательский сценарий.
Backend хранит данные и выполняет правила, которые нельзя доверять браузеру. Начните с поведения: пользователь создаёт задачу, читает её и завершает. Это вертикальный срез: один запрос проходит через проверку входа, бизнес-правило и хранилище.
Для первого выпуска Taskboard достаточно проектов и задач. Чат, совместное редактирование и рекомендации не нужны, чтобы проверить основной сценарий. Запишите также запрещённое поведение: чужой пользователь не может прочитать задачу; пустое название не сохраняется; повторный запрос не должен незаметно создавать дубли там, где обещана идемпотентность.
Требование становится полезным, когда можно предъявить наблюдаемый результат. «Быстрый и безопасный» не помогает написать тест. «Чужая задача не возвращается по её идентификатору» уже помогает.
Разбор на примере
Сценарий: Аня создаёт проект «Портфолио», добавляет задачу «Опубликовать API», завершает её. Борис получает отказ при запросе этой задачи. Это четыре проверяемых действия, а не список технологий.
Практическое задание
Напишите краткое описание Taskboard: кто пользователь, какие три действия доступны и какие три ситуации запрещены. Зафиксируйте то, что не входит в первую версию.
Критерии готовности
- У каждого действия есть вход и наблюдаемый результат.
- Есть отдельный сценарий доступа к чужой задаче.
- Первая версия не зависит от чата, платежей и микросервисов.
Проверка понимания
Какое требование можно сразу превратить в тест?
- Сервис должен быть современным
- Пользователь не может получить чужую задачу по ID
- Нужно использовать много технологий
Показать ответ и объяснение
Пользователь не может получить чужую задачу по ID
Проверяемое правило задаёт конкретную попытку и ожидаемый результат.
Источники
02. Проект и GitПоднять воспроизводимое окружение.
Репозиторий должен объяснять, как запустить проект на другой машине. Храните исходники, lockfile, инструкции и пример конфигурации без настоящих секретов. node_modules и локальная конфигурация в историю не входят. Маленький коммит соответствует одному понятному изменению: так проще найти поломку и отменить конкретную правку.
В package.json задайте type: module и команду start, запускающую src/server.js. Версию Node.js запишите в README. При первом добавлении зависимостей npm install создаёт lockfile; для воспроизведения окружения используйте npm ci. Не обновляйте все библиотеки одновременно с первой функциональной задачей: иначе неясно, какое изменение вызвало проблему.
Разбор на примере
Структура:
taskboard/
src/server.js
test/
package.json
package-lock.json
.gitignore
.env.example
README.md
В .gitignore добавьте node_modules/, .env и coverage/. Перед коммитом посмотрите git diff --staged.
Практическое задание
Создайте репозиторий Taskboard. Добавьте команду запуска и README, затем воспроизведите запуск из отдельной копии проекта.
Критерии готовности
- npm start имеет один документированный результат.
- В Git нет .env и node_modules.
- Из чистой копии зависимости устанавливаются по lockfile.
Проверка понимания
Что помогает воспроизводить набор зависимостей?
- Только папка node_modules
- Обновление всех пакетов при запуске
- Закоммиченный lockfile и npm ci
Показать ответ и объяснение
Закоммиченный lockfile и npm ci
Lockfile фиксирует разрешённые версии; npm ci устанавливает их без произвольного обновления.
Источники
03. Запрос и ответРазличать транспорт, маршрут и результат операции.
HTTP-запрос содержит метод, адрес, заголовки и иногда тело. GET читает представление ресурса; POST обычно создаёт ресурс или запускает действие; PATCH меняет часть состояния. Сервер отвечает статусом, заголовками и телом. Content-Type объясняет формат тела, а не гарантирует его корректность.
Разделяйте сетевой сбой и ответ приложения. При 404 сервер ответил, но ресурс не найден. При тайм-ауте клиент может не знать, успела ли операция выполниться. Поэтому автоматический повтор записи требует отдельного решения. Не возвращайте 200 для любого исхода: клиенту и мониторингу тогда сложно отличить успех от отказа.
Открыть в полном размере ↗Разбор на примере
POST /tasks HTTP/1.1
Content-Type: application/json
{"title":"Опубликовать API"}
Успех: 201 и созданная задача. Пустое название: 400 с кодом ошибки. Несуществующий маршрут: 404. Отказ в доступе не превращается в пустой успешный объект.
Практическое задание
Составьте таблицу методов, адресов и статусов для создания, чтения, изменения и удаления задач. Выполните GET через curl.exe или HTTP-клиент.
Критерии готовности
- Есть отдельные исходы 201, 400 и 404.
- Вы можете объяснить разницу между тайм-аутом и HTTP-ошибкой.
- Повтор POST не объявлен безопасным без реализации.
Проверка понимания
Клиент получил тайм-аут после POST. Что известно о записи?
- Она точно не произошла
- Она могла произойти до потери ответа
- Сервер обязательно удалит её
Показать ответ и объяснение
Она могла произойти до потери ответа
Потеря ответа не доказывает отсутствие побочного эффекта.
Источники
04. Первый серверЗапустить и проверить небольшой HTTP-обработчик.
Начните со стандартного HTTP-модуля, чтобы увидеть границу между запросом и ответом без скрытой магии фреймворка. Обработчик вызывается для каждого запроса. Ответ нужно завершить; иначе клиент продолжит ждать. Разбор URL нужен, чтобы параметры запроса не смешивались с путём.
На этом шаге сервис локальный и отвечает только на health. Это не показатель готовности базы и не полноценный бизнес-сценарий. Unknown route обязан возвращать 404. После проверки можно выбрать привычный фреймворк, сохранив тот же внешний контракт. Не открывайте учебный сервер в интернет до реализации доступа и ограничений.
Разбор на примере
Сохраните в src/server.js:
import {createServer} from "node:http";
const server = createServer((req, res) => {
const path = new URL(req.url, "http://localhost").pathname;
const ok = req.method === "GET" && path === "/health";
res.writeHead(ok ? 200 : 404, {"Content-Type": "application/json"});
res.end(JSON.stringify(ok ? {status: "ok"} : {error: "not_found"}));
});
server.listen(3000, "127.0.0.1");
Практическое задание
Запустите сервер и проверьте /health, /missing и POST /health. Добавьте автоматизированную или записанную ручную проверку статусов.
Критерии готовности
- /health отвечает 200 с JSON.
- Неизвестный путь и неподдерживаемый метод не получают успешный ответ.
- После остановки процесса порт освобождается.
Проверка понимания
Что означает успешный /health в этом примере?
- База и все операции проверены
- Пользователь авторизован
- HTTP-процесс отвечает на конкретный маршрут
Показать ответ и объяснение
HTTP-процесс отвечает на конкретный маршрут
Пример проверяет только процесс и обработчик; зависимости ещё не участвуют.
Источники
05. Контракт и валидацияНе пропускать произвольные данные в бизнес-логику.
Клиентские данные считаются непроверенными даже при наличии формы с обязательными полями. На сервере проверьте форму объекта, типы, границы длины и допустимые значения. Установите ограничение размера тела до накопления его целиком. Невалидный JSON и валидный JSON неправильной формы — разные случаи, но оба не должны падать в необработанную ошибку.
Не присваивайте поля тела объекту базы целиком: злоумышленник может прислать ownerId или служебный статус. Явно выберите разрешённые поля. Проверку формы отделяйте от правил: «title — строка» относится к входу, «задача принадлежит этому проекту» требует данных.
Разбор на примере
Чистая функция для первого POST:
export function parseTask(body) {
if (!body || typeof body.title !== "string") throw new Error("invalid_title");
const title = body.title.trim();
if (!title || title.length > 120) throw new Error("invalid_title");
return {title};
}
HTTP-слой преобразует известную ошибку в 400, а неизвестную — в 500 без внутренних подробностей.
Практическое задание
Добавьте POST /tasks с локальным массивом и parseTask. Проверьте null, число, пустую строку, слишком длинное название и посторонний ownerId.
Критерии готовности
- Созданной задаче сервер присваивает ID.
- В ответ не попадают произвольные поля клиента.
- Неправильный ввод не завершает процесс.
Проверка понимания
Откуда брать ownerId при создании задачи?
- Из проверенной сервером сессии
- Из JSON клиента
- Из названия проекта
Показать ответ и объяснение
Из проверенной сервером сессии
Клиент выбирает содержание операции, но не свою подтверждённую идентичность.
Источники
2. Данные и корректность
06. Модель данныхПревратить сценарий в таблицы и ограничения.
Массив теряется при перезапуске. Постоянное хранилище задаёт структуру и защищает инварианты независимо от конкретного обработчика. В Taskboard задача принадлежит проекту, проект — пользователю. Внешний ключ запрещает ссылку на отсутствующий проект, NOT NULL запрещает отсутствие обязательного значения, CHECK ограничивает статус.
Не храните одно и то же правило только в JavaScript: при конкурентных запросах две проверки могут пройти одновременно. Уникальность должна обеспечиваться базой, если нарушение недопустимо. Начните с минимальных таблиц. Поля «на всякий случай» усложняют миграции и маскируют неясную модель.
Разбор на примере
Фрагмент схемы после создания projects:
CREATE TABLE tasks (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
project_id bigint NOT NULL REFERENCES projects(id),
title text NOT NULL CHECK (length(trim(title)) BETWEEN 1 AND 120),
status text NOT NULL DEFAULT 'todo' CHECK (status IN ('todo', 'done')),
created_at timestamptz NOT NULL DEFAULT now()
);
Практическое задание
Спроектируйте users, projects и tasks. Создайте таблицы в локальной базе и попробуйте вставить задачу с несуществующим project_id.
Критерии готовности
- Все таблицы имеют первичные ключи.
- Несуществующий проект и недопустимый статус отклоняются базой.
- Вы объясняете, что происходит с задачами при удалении проекта.
Проверка понимания
Что защищает от ссылки на несуществующий проект?
- Название столбца project_id
- Внешний ключ
- Комментарий в README
Показать ответ и объяснение
Внешний ключ
Именно ограничение FOREIGN KEY проверяет существование связанной записи.
Источники
07. SQL без склейки строкПодключить хранилище и безопасно передавать параметры.
SQL описывает нужные данные, а планировщик выбирает способ их получить. Значения пользователя передавайте параметрами, не собирайте запрос конкатенацией. Это отделяет синтаксис SQL от данных. Названия таблиц и направление сортировки обычно не параметризуются как значения: для них нужен заранее заданный список вариантов.
Используйте пул соединений. Отдельное новое подключение на каждый запрос дорого; бесконечный пул способен исчерпать ресурсы базы. HTTP-обработчик вызывает функцию репозитория и преобразует результат в ответ, а не знает детали подключения. Пароль базы хранится в локальной конфигурации, не в файле исходников.
Разбор на примере
После npm install pg настройте DATABASE_URL локально:
import pg from "pg";
const pool = new pg.Pool({connectionString: process.env.DATABASE_URL, max: 5});
const result = await pool.query(
"SELECT id, title, status FROM tasks WHERE project_id = $1 ORDER BY id LIMIT $2",
[projectId, 20],
);
projectId здесь уже получен из проверенного ввода и проверенного доступа к проекту.
Практическое задание
Замените массив на PostgreSQL. Добавьте создание и чтение задач параметризованными запросами. Перезапустите Node.js и повторите чтение.
Критерии готовности
- Данные переживают перезапуск приложения.
- Кавычка в названии не ломает запрос.
- Соединения переиспользуются и корректно закрываются в тестах.
Проверка понимания
Как передавать пользовательское название задачи в SQL?
- Вставить в строку запроса
- Удалить кавычки и вставить
- Передать отдельным параметром
Показать ответ и объяснение
Передать отдельным параметром
Параметры сохраняют различие между текстом запроса и пользовательскими значениями.
Источники
08. Миграции схемыИзменять базу воспроизводимо.
Изменение таблицы вручную работает на одной машине, но не объясняет состояние остальных окружений. Миграция — упорядоченное изменение схемы с записью о применении. Уже выполненные миграции не переписывают: новое требование оформляют следующим шагом. Тестируйте переход и на пустой базе, и на базе с существующими записями.
Добавление обязательного столбца требует значения для старых строк. Для крупной таблицы заполнение и ограничение часто разделяют. Откат кода не обязательно означает откат данных: удаление столбца может уничтожить информацию. Совместимые расширения схемы позволяют временно работать старой и новой версии приложения.
Разбор на примере
Для учебной небольшой базы:
ALTER TABLE tasks ADD COLUMN priority integer DEFAULT 0;
UPDATE tasks SET priority = 0 WHERE priority IS NULL;
ALTER TABLE tasks ALTER COLUMN priority SET NOT NULL;
ALTER TABLE tasks ADD CONSTRAINT task_priority CHECK (priority BETWEEN 0 AND 3);
В журнале миграций хранится идентификатор этого изменения. Не запускайте такой файл заново при каждом HTTP-запросе.
Практическое задание
Добавьте priority миграцией. Проверьте старые записи и настройте однократное применение через выбранный инструмент миграций или небольшой журнал версий.
Критерии готовности
- Старая задача получила допустимое значение.
- Чистая база создаётся всей цепочкой миграций.
- Повторный запуск не пытается добавить столбец второй раз.
Проверка понимания
После публикации изменилась схема. Как исправить прошлую миграцию?
- Удалить историю базы
- Добавить следующую миграцию
- Переписать уже применённый файл
Показать ответ и объяснение
Добавить следующую миграцию
Следующая миграция делает переход явным для всех окружений.
Источники
09. Транзакции и гонкиСохранять связанные изменения вместе.
Транзакция объединяет операции: либо все изменения фиксируются, либо откатываются. Это нужно, когда завершение задачи создаёт запись аудита. Если первая запись прошла, а вторая нет, история расходится с состоянием. Все команды транзакции выполняются на одном соединении. Вызовы pool.query могут взять разные соединения и не образуют одну транзакцию.
Транзакция сама по себе не устраняет любую гонку. Два клиента могут прочитать старое состояние. Для перехода todo → done используйте условный UPDATE и проверяйте количество изменённых строк. Победитель записывает событие, повторный вызов возвращает уже достигнутое состояние. Внешнее письмо не откатится вместе с SQL: его отправку выносите за границу транзакции.
Разбор на примере
Последовательность на одном клиенте:
BEGIN;
UPDATE tasks SET status = 'done' WHERE id = 42 AND status = 'todo' RETURNING id;
-- Только при изменённой строке: INSERT в task_events.
COMMIT;
При ошибке выполните ROLLBACK; release соединения — в finally. Проверка владельца должна входить в операцию или предшествующий проверенный контекст.
Практическое задание
Добавьте аудит завершений. Смоделируйте ошибку вставки события и два одновременных завершения одной задачи.
Критерии готовности
- При ошибке аудита изменение статуса откатывается.
- Два запроса не создают два события одного перехода.
- Соединение освобождается и при исключении.
Проверка понимания
Где должны исполняться команды одной транзакции?
- На одном соединении
- На любом соединении пула
- На разных соединениях для скорости
Показать ответ и объяснение
На одном соединении
Состояние транзакции принадлежит соединению с PostgreSQL.
Источники
10. Списки и индексыНе возвращать всю базу одним ответом.
Неограниченный список работает на десяти задачах и ломается на ста тысячах. Ограничьте размер страницы на сервере. Сортировка должна быть стабильной: created_at может совпадать, поэтому нужен второй ключ. Для начала удобно идти по возрастающему id и передавать последний увиденный ID как курсор.
Индекс ускоряет конкретный способ поиска, но занимает место и удорожает запись. Для списка задач проекта полезен составной индекс (project_id, id). Проверяйте план на репрезентативном объёме: последовательное чтение маленькой таблицы не означает поломку индекса. OFFSET прост для небольших списков, но глубокие страницы требуют пропустить много строк.
Разбор на примере
CREATE INDEX tasks_project_cursor ON tasks(project_id, id);
SELECT id, title, status FROM tasks
WHERE project_id = $1 AND id > $2
ORDER BY id LIMIT $3;
Запросите limit + 1 строку, чтобы определить hasMore. Клиенту верните не больше limit и курсор последней возвращённой задачи.
Практическое задание
Добавьте курсорную пагинацию с пределом 100. Проверьте первую, промежуточную и последнюю страницы на 250 задачах.
Критерии готовности
- Задачи не повторяются при последовательном обходе.
- Сервер ограничивает слишком большой limit.
- Есть план запроса и объяснение выбранного индекса.
Проверка понимания
Почему created_at без дополнительного ключа опасен для курсора?
- Дата всегда уникальна
- Несколько записей могут иметь одинаковое время
- По дате нельзя сортировать
Показать ответ и объяснение
Несколько записей могут иметь одинаковое время
При одинаковом времени нужен детерминированный второй ключ, чтобы не пропускать записи.
Источники
3. Пользователи и надёжность
11. Вход и сессияОтделить проверку пользователя от хранения его состояния.
Аутентификация отвечает, кто делает запрос. Для первого браузерного приложения удобно использовать непрозрачный случайный идентификатор сессии: браузер хранит cookie, сервер — пользователя и срок действия. Сессия должна прекращаться при выходе; после входа идентификатор обновляется. Пароли не хранят открыто и не защищают обычным SHA-256: используйте специализированную библиотеку хеширования паролей.
Cookie с HttpOnly недоступна JavaScript, Secure отправляет её по HTTPS, SameSite ограничивает часть межсайтовых сценариев. Эти флаги не заменяют проверку CSRF для выбранной схемы. Не реализуйте собственную криптографию. Для локальной практики сначала сделайте тестовый вход с фиксированным пользователем, явно недоступный в production, затем подключите библиотеку аутентификации.
Разбор на примере
Модель: session_id → user_id, expires_at, revoked_at. Сервер проверяет срок и отзыв при каждом запросе. Logout отзывает запись и удаляет cookie. Сохранённая ранее cookie после logout больше не даёт доступ.
Практическое задание
Добавьте вход, выход и /me через выбранную библиотеку. Для учебной проверки используйте два локальных аккаунта и не отправляйте настоящие пароли в логи.
Критерии готовности
- Без сессии /me отвечает 401.
- Просроченная и отозванная сессии отклоняются.
- Повторное использование cookie после выхода не восстанавливает доступ.
Проверка понимания
Достаточно ли удалить cookie только в браузере для отзыва серверной сессии?
- Да, копии cookie невозможны
- Да, если cookie длинная
- Нет, нужно прекратить действие сессии на сервере
Показать ответ и объяснение
Нет, нужно прекратить действие сессии на сервере
Скопированный идентификатор должен перестать работать независимо от браузера.
Источники
12. Доступ к объектамЗакрыть чтение и изменение чужих задач.
Наличие сессии не даёт доступ ко всем объектам. Авторизация проверяет конкретное действие над конкретным ресурсом. Условие «пользователь залогинен» пропускает атаку, в которой ID задачи заменяют на чужой. UUID тоже не заменяет проверку: сложность угадывания не является правом доступа.
В Taskboard проверка проходит через владельца проекта. Применяйте её к чтению, обновлению, удалению и спискам. Если endpoint возвращает вложенные данные, проверяйте и их принадлежность. Можно отвечать 404 вместо 403, чтобы не раскрывать существование чужого объекта; важно делать это последовательно. Тест с двумя пользователями полезнее проверки только счастливого пути.
Разбор на примере
Чтение задачи:
SELECT t.id, t.title, t.status FROM tasks t
JOIN projects p ON p.id = t.project_id
WHERE t.id = $1 AND p.owner_id = $2;
$2 берётся из сессии. Если строк нет, ответ — 404. Для UPDATE используйте такое же ограничение владельца, а не отдельный незащищённый запрос по id.
Практическое задание
Проверьте все операции Ани с задачами Бориса. Добавьте негативные тесты чтения, изменения и удаления.
Критерии готовности
- ID владельца никогда не берётся из тела запроса.
- Чужие задачи не видны в списках.
- Изменение и удаление защищены так же, как чтение.
Проверка понимания
Что защищает задачу с непредсказуемым UUID?
- Только секретность UUID
- Проверка права пользователя на объект
- Запрет кнопки в интерфейсе
Показать ответ и объяснение
Проверка права пользователя на объект
Сервер обязан проверять права независимо от формы идентификатора и интерфейса.
Источники
13. Ошибки и тайм-аутыДать клиенту понятный исход и ограничить ожидание.
Ошибка контракта, отсутствие ресурса и падение базы требуют разных действий. Верните клиенту стабильный code, короткое сообщение и requestId. Внутренний stack trace нужен разработчику в защищённом журнале, а не пользователю. Логи не должны содержать пароли, cookie и содержимое приватных задач.
Любой внешний вызов ограничивайте временем. Если зависимость зависла, она не должна удерживать запрос бесконечно. Повторять можно только временные ошибки и только с учётом побочных эффектов. Ошибку валидации повтор не исправит. Ограничьте число попыток, общий срок и добавьте задержку; иначе сбой зависимости превращается в лавину повторов.
Разбор на примере
Ответ клиенту:
{"error":{"code":"database_unavailable","message":"Повторите позже","requestId":"req-42"}}
Журнал с тем же requestId содержит тип ошибки и длительность, но не DATABASE_URL. Для внешнего fetch можно передать signal: AbortSignal.timeout(3000).
Практическое задание
Остановите локальную базу во время запросов. Сделайте ограниченное ожидание и единый формат ошибок. Сопоставьте ответ с журналом.
Критерии готовности
- Запрос завершается в установленный срок.
- В ответе нет stack trace и строки подключения.
- Валидационные ошибки не уходят в повтор.
Проверка понимания
Что следует повторять автоматически?
- Любую ошибку до успеха
- Невалидный JSON
- Ограниченный набор временных сбоев с безопасной семантикой
Показать ответ и объяснение
Ограниченный набор временных сбоев с безопасной семантикой
Повторы требуют и временного характера сбоя, и понимания последствий повторной операции.
Источники
14. Повтор без дублейОбработать повторную отправку операции.
Пользователь дважды нажал кнопку, клиент повторил POST после тайм-аута, прокси оборвал ответ — всё это обычные сценарии. Идемпотентность означает, что повтор одной операции не умножает её эффект. Ключ должен описывать попытку бизнес-действия, а не генерироваться заново при каждом сетевом повторе.
Храните ключ вместе с пользователем, типом операции, отпечатком входа и результатом. Уникальное ограничение защищает от двух параллельных обработчиков. Тот же ключ с другим телом отклоняется, иначе ошибка клиента вернёт результат чужого по смыслу запроса. Заранее определите срок хранения ключей: после его истечения прежние гарантии уже не действуют.
Открыть в полном размере ↗Разбор на примере
Аня отправляет POST /projects с ключом create-project-7. Ответ потерян. Повтор с тем же телом и ключом возвращает тот же projectId. Повтор с названием другого проекта и тем же ключом получает 409. Ключ Бориса не пересекается с ключом Ани.
Практическое задание
Добавьте идемпотентное создание проекта. В одной транзакции сохраните ключ и результат; проверьте два параллельных запроса.
Критерии готовности
- Создаётся ровно один проект.
- Ключ ограничен пользователем и операцией.
- Изменённое тело с прежним ключом отклоняется.
Проверка понимания
Где надёжно защититься от двух одновременных одинаковых ключей?
- Уникальным ограничением в базе
- Предварительным SELECT без ограничения
- Отключением кнопки
Показать ответ и объяснение
Уникальным ограничением в базе
Два SELECT могут одновременно не найти ключ. Уникальность арбитрируется базой.
Источники
15. Фоновые задачиВынести долгую работу из HTTP-запроса.
Экспорт тысячи задач не должен держать браузерное соединение открытым. Создайте запись задания, верните 202 с jobId и выполняйте работу отдельным worker. Пользователь проверяет состояние: queued, running, succeeded, failed. Результат выдаётся только владельцу задания.
Фоновая работа тоже падает: процесс завершился после записи файла, но до отметки succeeded. Поэтому обработчик должен переживать повтор. Нужны лимит попыток, срок аренды задания и способ вернуть зависшие running в обработку. Для первого проекта достаточно таблицы заданий. Очередь сама по себе не гарантирует однократный бизнес-эффект.
Разбор на примере
Сценарий: POST /exports → 202 {jobId}. Worker формирует файл под стабильным ключом export/jobId. Повтор перезаписывает тот же результат, а не создаёт второй экспорт с новым ID. GET /exports/jobId проверяет владельца.
Практическое задание
Добавьте фоновый экспорт задач в JSON. Остановите worker во время обработки и затем запустите снова.
Критерии готовности
- HTTP-запрос не ждёт завершения экспорта.
- Зависшее задание можно повторить.
- Чужой пользователь не получает статус и файл.
Проверка понимания
Worker может получить задание повторно. Что требуется?
- Надёжда на очередь
- Обработчик с безопасным повтором
- Новый jobId при каждом запуске
Показать ответ и объяснение
Обработчик с безопасным повтором
Повторная доставка нормальна; бизнес-эффект должен оставаться контролируемым.
Источники
4. Проверки и выпуск
16. Тесты поведенияДоказывать работу сценариев, а не наличие функций.
Разделите быстрые тесты чистых правил и интеграционные тесты с настоящей локальной PostgreSQL. Mock базы не обнаружит ошибку SQL, внешний ключ и гонку транзакций. Тест должен явно задавать исходное состояние, действие и проверяемый результат. Подготовка данных изолируется от других тестов.
В первую очередь проверьте создание задачи, отказ на плохой ввод, доступ двух пользователей, откат транзакции и повтор операции. Не гонитесь за процентом покрытия: можно выполнить все строки и не проверить ни одного важного результата. Тест, который проходит при намеренном удалении проверки владельца, не защищает доступ.
Разбор на примере
Пример чистого теста в test/validation.test.js:
import test from "node:test";
import assert from "node:assert/strict";
import {parseTask} from "../src/validation.js";
test("название очищается, ownerId не принимается", () => {
assert.deepEqual(parseTask({title: " API ", ownerId: "other"}), {title: "API"});
});
test("пустое название запрещено", () => {
assert.throws(() => parseTask({title: " "}));
});
Практическое задание
Соберите набор из пяти интеграционных сценариев. Временно сломайте проверку владельца локально и убедитесь, что соответствующий тест падает; затем восстановите код.
Критерии готовности
- Тесты повторяемы без ручной очистки базы.
- Чужой доступ проверяется настоящим HTTP-запросом.
- Намеренная ошибка действительно обнаруживается.
Проверка понимания
Что лучше доказывает защиту доступа?
- Тест числа строк файла
- Успешный вход одного пользователя
- Запрос пользователя Б к задаче А с проверкой отказа
Показать ответ и объяснение
Запрос пользователя Б к задаче А с проверкой отказа
Негативный межпользовательский сценарий проверяет само правило.
Источники
17. Docker без потери данныхСобрать воспроизводимый запуск.
Образ фиксирует runtime и файлы приложения, контейнер — конкретный запущенный процесс. Записи во временный слой контейнера не являются надёжным хранилищем. PostgreSQL подключайте к отдельному volume; секреты передавайте при запуске. Не копируйте .env в образ.
Для нашего JavaScript-проекта компилятор не нужен. Копируйте package-файлы перед исходниками, чтобы изменение сервера не сбрасывало кеш зависимостей. Запускайте процесс непривилегированным пользователем. В контейнере сервер слушает 0.0.0.0; это не то же самое, что публиковать порт базы наружу. Публикацию портов определяет конфигурация запуска.
Разбор на примере
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY src ./src
USER node
CMD ["node", "src/server.js"]
Для воспроизводимого релиза закрепите базовый образ по выбранному digest. В .dockerignore исключите .env, node_modules, .git. Серверу задайте адрес прослушивания через конфигурацию.
Практическое задание
Соберите образ Taskboard и запустите с локальной базой. Пересоздайте контейнер приложения и убедитесь, что задачи сохранились.
Критерии готовности
- Приложение отвечает через опубликованный локальный порт.
- Секретов нет в образе и исходниках.
- Данные PostgreSQL переживают пересоздание её контейнера с тем же volume.
Проверка понимания
Что хранит данные PostgreSQL независимо от контейнера?
- Неизменяемый слой образа
- Отдельный постоянный volume
- Кеш npm
Показать ответ и объяснение
Отдельный постоянный volume
Данные базы должны находиться в постоянном хранилище, подключаемом новому контейнеру.
Источники
18. Поставка и миграцияПроверить релиз до переключения пользователей.
CI повторяет одни и те же проверки для каждого изменения: установка зависимостей, тесты, сборка артефакта. CD переносит проверенный артефакт в окружение. Если пересобрать «то же самое» отдельно в production, вы проверяли уже не тот артефакт. Фиксируйте идентификатор сборки и конфигурацию запуска.
Перед релизом проверьте совместимость миграции с предыдущей версией. Сначала добавьте новое поле, затем выпустите код, и лишь позже удаляйте старое. Откат приложения не возвращает удалённые данные. Для учебного проекта проведите репетицию локально: старая версия → миграция → новая версия → возврат старой версии на расширенной схеме.
Разбор на примере
Минимальная последовательность: npm ci → node --test → сборка образа → запуск с тестовой базой → HTTP smoke → сохранение версии образа. Проверка smoke создаёт и читает задачу, а не ограничивается /health.
Практическое задание
Опишите и воспроизведите релиз с добавлением priority. Проверьте рабочий сценарий до и после миграции и подготовьте команду возврата предыдущего образа.
Критерии готовности
- Артефакт идентифицируется тегом или digest.
- Есть проверка создания и чтения задачи.
- Понятно, какие изменения данных не отменяются откатом кода.
Проверка понимания
Что нужно проверить до отката приложения?
- Только цвет интерфейса
- Совместимость старого кода с текущей схемой базы
- Количество коммитов
Показать ответ и объяснение
Совместимость старого кода с текущей схемой базы
Данные и схема могли измениться независимо от версии приложения.
Источники
19. Логи и восстановлениеУвидеть сбой и восстановить данные на практике.
Пользователь сообщает «задача не сохранилась». Для расследования нужны requestId, маршрут, статус, длительность и версия приложения. Не сохраняйте тело каждого запроса: там могут быть личные данные. Метрики отвечают на общий вопрос «растёт ли доля ошибок», логи — на вопрос о конкретном событии.
Измеряйте скорость ответа и частоту ошибок; среднее скрывает медленные запросы, поэтому смотрите распределение. Сигнал должен приводить к действию: проверить доступность базы, лимит соединений или выпуск. Резервная копия полезна только после успешного восстановления. В учебном локальном окружении можно сделать дамп самостоятельно; платный сервис для упражнения не нужен.
Разбор на примере
Событие:
{"requestId":"r7","route":"POST /tasks","status":201,"durationMs":24,"release":"v1"}
Репетиция восстановления: дамп локальной учебной базы → новая отдельная база → восстановление → сравнение количества проектов и задач → HTTP-чтение. Не восстанавливайте поверх единственной рабочей копии.
Практическое задание
Добавьте структурированный журнал. Смоделируйте недоступную базу, найдите запрос по ID и восстановите учебный дамп в отдельную базу.
Критерии готовности
- Журнал не содержит cookie, пароли и названия приватных задач.
- Сбой можно связать с конкретным запросом.
- Проверено чтение восстановленных данных.
Проверка понимания
Что подтверждает пригодность резервной копии?
- Файл существует
- У него красивое имя
- Восстановление в отдельную базу и проверка данных
Показать ответ и объяснение
Восстановление в отдельную базу и проверка данных
Сам факт создания файла не доказывает его полноту и восстанавливаемость.
Источники
20. Защита проектаСобрать доказательства готовности API.
Финал маршрута — проверяемый проект, который может запустить другой человек. README описывает установку, конфигурацию без секретов, миграции, тестовые аккаунты и основные запросы. Укажите ограничения: какие сценарии не реализованы и какую нагрузку вы действительно проверяли.
Пройдите демонстрацию с чистого состояния. Аня создаёт проект и задачу, завершает её; Борис не может её прочитать; перезапуск не теряет запись; повтор создания не создаёт дубль. Затем сломайте зависимость и покажите понятную ошибку. Именно эти проверки объясняют качество backend лучше, чем длинный список технологий. Kubernetes, кеш и микросервисы добавляйте, когда сможете назвать конкретное ограничение текущего решения.
Разбор на примере
Чек-лист демонстрации: пустая база → миграции → запуск → вход Ани → создание → доступ Бориса → повтор → перезапуск → чтение. Для каждого пункта сохраните команду и ожидаемый статус; секретные значения замените локальными тестовыми.
Практическое задание
Подготовьте README и короткую демонстрацию Taskboard. Выполните весь сценарий в чистом локальном окружении и перечислите три дальнейших улучшения с причинами.
Критерии готовности
- Другой человек запускает проект по README.
- Есть позитивные и негативные проверки доступа.
- Дальнейшие технологии привязаны к измеренной проблеме, а не к моде.
Проверка понимания
Что является лучшим основанием добавить кеш?
- Так делают все
- Измеренная задержка повторяющегося чтения и определённые правила устаревания
- В проекте мало зависимостей
Показать ответ и объяснение
Измеренная задержка повторяющегося чтения и определённые правила устаревания
Оптимизация требует измерения и понимания того, когда данные могут устареть.