Читать книгу Full stack Developer - Группа авторов - Страница 3
Раздел I. Подготовка: инструменты, репо, “скелет книги”
Глава 1. Как развернуть окружение
ОглавлениеНиже – минимальный набор инструментов, чтобы у вас работало всё: локальный запуск, тесты, генерация кода из OpenAPI и CI.
1.1. macOS / Linux / Windows (WSL2)
macOS
Обычно достаточно:
– терминал (встроенный или iTerm2),
– пакетный менеджер (например, Homebrew),
– Docker Desktop.
Linux
Удобнее всего, когда:
– есть нормальный bash/zsh,
– Docker установлен нативно,
– вы добавили пользователя в группу docker, чтобы не писать sudo на каждый запуск.
Windows
Рекомендуемый путь – WSL2:
– ставите WSL2,
– ставите Ubuntu,
– работаете внутри Linux‑окружения,
– Docker Desktop на Windows подключается к WSL2.
Так вы избегаете большинства проблем с путями, правами, файловой системой и скоростью работы инструментов.
1.2. Git, SSH, GPG (подпись коммитов)
Git
Проверьте:
bash
git –version
Настройте имя и почту (один раз):
bash
git config –global user.name "Your Name"
git config –global user.email "you@example.com"
SSH (для доступа к удалённым репозиториям)
Создайте ключ:
bash
ssh-keygen -t ed25519 -C "you@example.com"
Добавьте ключ в ssh-agent:
bash
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Дальше публичный ключ (~/.ssh/id_ed25519.pub) добавляется в ваш Git‑хостинг.
GPG (подпись коммитов)
Подпись коммитов – это не “обязательная красота”, а способ снизить риск подмены авторства и упростить аудит изменений. Если у вас в команде принято подписывать коммиты – включайте сразу.
– Установите GPG (на macOS часто через brew, на Linux через пакетный менеджер).
– Сгенерируйте ключ:
bash
gpg –full-generate-key
– Узнайте ID ключа:
bash
gpg –list-secret-keys –keyid-format=long
– Включите подпись коммитов:
bash
git config –global commit.gpgsign true
git config –global user.signingkey <YOUR_KEY_ID>
Если подпись не нужна – можно пропустить. На содержание книги это не влияет, но полезно как привычка.
1.3. Docker и Docker Compose
Docker в этой книге – это в первую очередь инфраструктура: база данных, кеш, брокеры, и всё, что нужно нескольким приложениям одновременно.
Проверьте:
bash
docker –version
docker compose version
Мы будем использовать команду нового формата:
bash
docker compose up -d
Важно: приложения (NestJS/FastAPI/Spring/Go) можно запускать и локально, и в контейнерах. В книге мы будем чаще запускать их локально, а инфраструктуру – в контейнерах. Это обычно быстрее для разработки.
1.4. Языки и менеджеры версий
Наша цель – чтобы версии инструментов были повторяемыми. Для этого хорошо иметь менеджер версий на каждый язык.
Node.js: nvm
Для TypeScript и Next.js нам нужен Node.js и pnpm.
– Установите nvm.
– Поставьте нужную версию Node:
bash
nvm install 20
nvm use 20
– Включите pnpm (через corepack):
bash
corepack enable
corepack prepare pnpm@latest –activate
pnpm –version
Почему pnpm: он быстрее и экономнее по диску, особенно в монорепозитории.
Python: pyenv или uv
Есть два популярных пути:
Вариант А: pyenv – классический менеджер версий Python.
Вариант Б: uv – современный быстрый инструмент, который часто заменяет и pip, и venv.
Для книги подойдёт любой. Если хочется проще и быстрее по зависимостям – выбирайте uv.
Минимально важно: чтобы python и pip были предсказуемой версии, а зависимости проекта ставились в изолированное окружение.
Java/Kotlin: sdkman
Для Spring Boot (и Kotlin) удобно использовать sdkman:
– ставим sdkman,
– выбираем JDK (например, 21):
bash
sdk install java 21.0.2-tem
java -version
Go: asdf (или родной установщик)
Go можно ставить напрямую, но для управления версиями удобен asdf:
– ставим asdf,
– ставим Go нужной версии,
– проверяем:
bash
go version
Если asdf не хочется – поставьте Go из официального дистрибутива, это тоже нормально.
0.1.5. IDE и плагины
VS Code (как “универсальный редактор”)
VS Code удобен тем, что в одном окне можно держать весь монорепозиторий.
Рекомендуемые плагины:
– ESLint, Prettier (TS/JS)
– Prisma (подсветка схем)
– Python (официальный)
– Pylance
– Go (официальный)
– Java Extension Pack (если хотите работать с JVM в VS Code)
– OpenAPI (подсветка и валидация YAML)
– Docker
IntelliJ IDEA (для JVM)
Если вы всерьёз делаете Spring Boot/Kotlin – IntelliJ IDEA будет самым комфортным вариантом: автодополнение, дебаг, Gradle/Maven, рефакторинги.
GoLand (опционально)
Хорош для Go, но не обязателен: VS Code + Go plugin тоже отлично справляется.
0.1.6. Devcontainer: добавлять сразу?
Да, добавляем сразу, но как необязательный способ запуска.
Идея devcontainer:
– репозиторий содержит описание окружения,
– VS Code может открыть проект “в контейнере”,
– версии Node/Python/Go/Java будут одинаковыми у всех.
При этом:
– обычный запуск через терминал и docker-compose остаётся “по умолчанию”,
– devcontainer – это страховка и удобство.
В репозитории обычно появляются:
– .devcontainer/devcontainer.json
– Dockerfile для dev‑образа (если нужно)
– инструкции по установке расширений и запуску compose.
Монорепозиторий и структура
Мы используем монорепозиторий: один Git‑репо, внутри которого живут фронт, несколько бэкендов, общие пакеты и инфраструктура.
Почему так удобно именно для сравнения:
– один контракт OpenAPI на всех,
– одни e2e‑тесты на всех,
– проще переиспользовать модели, генераторы, утилиты,
– проще увидеть различия в коде рядом.
Мы договорились про:
– pnpm как менеджер пакетов,
– Turborepo как инструмент сборки/кеширования задач.
0.2.1. Договоримся о путях
Структура, которую мы будем использовать:
– /apps/frontend-next – Next.js (App Router)
– /apps/api-ts – TypeScript backend (NestJS)
– /apps/api-py – Python backend (FastAPI)
– /apps/api-java – Spring Boot + Kotlin
– /apps/api-go – Go + chi
– /packages/shared-contract – OpenAPI/DTO/генерация клиентов
– /infra – docker-compose, (позже – k8s/terraform при необходимости)
– /tests/e2e – одни тесты на все API
Важно: мы специально называем приложения одинаково предсказуемо (api-ts, api-py, …), чтобы скрипты, CI и документация были проще.
2.2. Turborepo: зачем он здесь
Turborepo полезен, когда:
– много проектов,
– есть повторяющиеся команды (lint, test, build),
– хочется кешировать результаты и запускать параллельно.
Типичный подход:
– каждый проект в apps/ имеет свои команды,
– корневые команды запускают их все,
– turbo решает, что можно выполнить параллельно, а что нужно последовательно.
Например, логика может быть такая:
– сначала генерируем типы из OpenAPI в packages/shared-contract,
– потом бэкенды используют эти артефакты,
– потом фронт использует клиент.
Даже если вы не используете сложные графы задач – turbo всё равно упрощает запуск “всего репо”.
2.3. Общие правила репозитория
Чтобы репо не превратилось в свалку:
1) Все внешние сервисы (Postgres/Redis и т.п.) – в /infra и docker-compose.
2) Контракт API – в одном месте (/packages/shared-contract).
3) Каждый backend должен:
– подниматься командой dev,
– отдавать OpenAPI (если фреймворк умеет),
– запускать миграции отдельной командой.
4) E2E тесты не знают, какой язык под капотом – им важен URL и контракт.
Контракт как основа: OpenAPI-first
Самая важная идея книги: контракт важнее реализации.
Мы будем строить систему так, будто бэкенд – это “плагин”. Сегодня он на NestJS, завтра на FastAPI, послезавтра на Go. Если контракт не меняется – фронт и интеграции не должны страдать.
0.3.1. Что значит OpenAPI-first
OpenAPI-first означает:
1) Сначала описываем API в openapi.yaml.
2) На основе контракта:
– генерируем клиент для фронта,
– генерируем типы/DTO (где это уместно),
– валидируем, что запросы/ответы соответствуют схеме.
3) Реализации бэкенда обязаны соответствовать контракту.
Это дисциплина, которая резко снижает хаос:
– меньше “а давай добавим поле, а фронт потом как-нибудь догадается”,
– меньше разночтений между командами,
– проще писать e2e‑тесты.
Где лежит контракт и как он выглядит
Контракт кладём в:
– /packages/shared-contract/openapi.yaml
Внутри openapi.yaml мы описываем:
– базовую информацию (title, version),
– серверы (на локалке),
– пути (/health, /users, …),
– схемы данных (DTO),
– ошибки (единый формат).
Даже если в каждом бэкенде есть автогенерация OpenAPI (NestJS Swagger, FastAPI docs, Springdoc) – источником истины остаётся наш openapi.yaml.
Автогенерация полезна, но она часто:
– зависит от аннотаций и кода,
– по-разному описывает типы,
– может “уплывать” при рефакторингах.
В книге мы будем делать наоборот: код подстраивается под контракт.
Генерация клиентов и типов
Зачем это нужно:
– фронту нужны типы запросов/ответов,
– e2e‑тестам нужны типы,
– иногда удобно генерировать серверные интерфейсы/заготовки.
Что можно генерировать:
– TypeScript клиент (например, fetch‑клиент или axios‑клиент),
– TypeScript типы DTO,
– (опционально) клиенты для других языков.
В рамках книги важно не то, какой именно генератор вы выберете, а сам принцип:
– контракт обновили → артефакты пересобрали → всё компилируется/тестируется.
Хорошая практика: хранить сгенерированный код либо:
– в packages/shared-contract/dist (и не коммитить, генерировать в CI), либо
– коммитить в репозиторий (проще для новичков, меньше магии).
Для учебной книги часто удобнее второй вариант (видно, что получилось). Для промышленной разработки обычно выбирают первый (генерация в CI).
Валидация контракта в CI
Контракт должен проверяться автоматически, иначе он быстро перестанет быть “истиной”.
Минимальный набор проверок:
1) Линт OpenAPI – формат, обязательные поля, правила стиля.
2) Валидация схемы – файл реально соответствует OpenAPI версии 3.x.
3) Проверка совместимости (опционально) – например, чтобы не ломать обратную совместимость без явного решения.
Даже без сложных проверок уже полезно, чтобы CI падал, если openapi.yaml просто “сломался”.
0.3.5. Как связываем контракт и реализации
Чтобы это не осталось словами, нам нужны практические “крючки” в коде:
– Frontend использует сгенерированный клиент/типы. Если контракт поменяли – фронт компилится или падает, и это хорошо.
– E2E тесты используют контракт как основу: например, проверяют, что эндпоинты существуют, и ответы соответствуют схеме.
– Backend на каждом языке:
– реализует маршруты, перечисленные в контракте,
– возвращает формат ошибок, описанный в контракте,
– держит совместимость.
В следующих разделах мы начнём с самого простого: GET /health, затем добавим сущность (например, users или todos) и постепенно усложним.
Итог раздела: что у нас должно получиться к концу подготовки
К этому моменту у вас должно быть:
1) Готовое окружение:
– Git настроен,
– Docker работает,
– Node + pnpm работают,
– Python/Go/Java доступны (или через devcontainer).
2) Монорепозиторий с понятной структурой:
– apps/ для приложений,
– packages/shared-contract для контракта,
– infra для docker-compose,
– tests/e2e для общих тестов.
3) Принцип OpenAPI-first:
– есть openapi.yaml,
– есть генерация артефактов,
– есть проверка/валидация контракта.
Дальше мы начнём “первую реальную вертикаль”: инфраструктура (Postgres в compose), минимальные сервисы на каждом языке и первый общий e2e‑тест, который прогоняется одинаково для любого API.
Монорепозиторий и структура
В этой книге мы будем собирать один и тот же продукт, но с разными бэкендами: на TypeScript, Python, JVM и Go. Чтобы не утонуть в папках, скриптах и «а где это лежит?», мы сразу выберем понятную модель организации кода – монорепозиторий.
Монорепозиторий – это когда весь код проекта живёт в одном Git‑репозитории: фронтенд, несколько бэкендов, общие библиотеки, тесты и инфраструктура. Звучит как «большая куча», но при правильной структуре это, наоборот, делает проект спокойнее и предсказуемее.
Почему монорепозиторий удобен именно в этой книге
У нас будет несколько реализаций одного API. Они должны:
– иметь одинаковые эндпоинты и одинаковые форматы запросов/ответов;
– проходить одни и те же e2e‑тесты;
– использовать одни и те же договорённости по ошибкам, статус-кодам, структуре JSON.
Если каждую реализацию держать в отдельном репозитории, то почти всё усложняется:
– контракт API начинает жить в нескольких копиях;
– тесты дублируются и расходятся;
– инфраструктура (Postgres/Redis и т.п.) повторяется;
– сравнивать реализации сложнее – всё раскидано по разным местам.
Монорепо решает это простым способом: всё рядом. В результате вы быстрее видите, чем отличаются подходы, а не боретесь с организацией.
Общая структура проекта
Мы используем фиксированную структуру папок. Она будет одинаковой на протяжении всей книги:
text
/apps/frontend-next
/apps/api-ts
/apps/api-py
/apps/api-java
/apps/api-go
/packages/shared-contract
/infra
/tests/e2e
Дальше разберём смысл каждой папки, и какие правила мы будем соблюдать.
apps/: приложения (то, что запускается)
Папка apps/ содержит запускаемые приложения: фронтенд и несколько вариантов бэкенда.
/apps/frontend-next
Фронтенд на Next.js. Это «лицо» продукта и удобный способ быстро проверить, что API реально работает.
Что обычно лежит внутри:
– страницы и компоненты UI;
– клиент для API (чаще всего сгенерированный);
– настройки окружений (.env.local), базовый URL до API.
Главная договорённость: фронтенд не должен знать, какой именно бэкенд работает внутри. Он должен опираться на контракт: если API соответствует OpenAPI, фронтенд работает.
/apps/api-ts
Бэкенд на TypeScript (например, NestJS). Он будет полезен как «референс» для многих команд: TypeScript часто выбирают в компаниях, где много фронтенда.
Обычно внутри:
– контроллеры/роуты;
– сервисы (бизнес-логика);
– слой доступа к данным;
– конфиг приложения и переменные окружения.
/apps/api-py
Бэкенд на Python (например, FastAPI). Хорош для скорости разработки, удобных деклараций схем и читабельного кода.
Обычно внутри:
– роуты (эндпоинты);
– pydantic‑модели (или аналоги);
– зависимости (DI), конфиги;
– миграции БД (если используем).
/apps/api-java
Бэкенд на JVM (чаще всего Spring Boot; в книге мы можем использовать Java или Kotlin). Это классический вариант для корпоративных систем.
Обычно внутри:
– контроллеры;
– сервисы и репозитории;
– конфигурация, профили окружений;
– сборка (Gradle/Maven).
/apps/api-go
Бэкенд на Go (например, chi или gin). Его сильные стороны – производительность, простота деплоя, предсказуемое потребление памяти.
Обычно внутри:
– роутер и хендлеры;
– слой бизнес-логики;
– слой хранения;
– конфиг и сборка в один бинарник.
Общее правило для apps/*
Каждое приложение в apps/ должно уметь:
1. Запускаться в dev‑режиме одной командой (например, dev).
2. Поднимать соединение с инфраструктурой из infra (Postgres/Redis).
3. Иметь health-check (например, GET /health).
4. Иметь настройку порта через переменные окружения.
Эти правила сильно упрощают e2e‑тесты и CI: тестам не важно, на чём написан API – важно, что он доступен и соответствует контракту.
packages/: общее и переиспользуемое
Если apps/ – это «то, что мы запускаем», то packages/ – это «то, что мы переиспользуем».
/packages/shared-contract
Это одна из ключевых папок в книге. Здесь живёт контракт между фронтом и бэкендом – OpenAPI‑описание, а также всё, что из него генерируется.
Типичное содержимое:
– openapi.yaml – главный файл контракта;
– папка со сгенерированными TypeScript‑типами;
– сгенерированный API‑клиент для фронтенда и тестов;
– скрипты для генерации (чтобы процесс был одинаковым у всех).
Почему это важно:
– контракт лежит в одном месте;
– фронтенд и тесты используют один и тот же источник;
– мы уменьшаем шанс «фронт ожидал одно, бэкенд вернул другое».
Правило: если меняется API, сначала меняем OpenAPI в shared-contract, потом обновляем реализации.
infra/: инфраструктура проекта
Папка infra/ – это всё, что не является кодом приложения, но нужно для работы системы.
Что здесь может быть
– docker-compose.yml для локальной инфраструктуры (Postgres, Redis, очереди);
– конфиги и скрипты инициализации;
– папки под Kubernetes‑манифесты (если дойдём до них);
– Terraform‑файлы (если будем описывать облачную инфраструктуру).
На старте нам чаще всего достаточно docker-compose, потому что это быстрый способ поднять окружение, одинаковое у всех.
Почему инфраструктура в отдельной папке?
Чтобы она была общей для всех бэкендов. Нам не нужно четыре отдельных compose‑файла – иначе мы будем править одно и то же в четырёх местах.
tests/e2e: одни тесты на все API
Папка /tests/e2e содержит end-to-end тесты, которые запускаются против любого из бэкендов. Это важный принцип книги: мы сравниваем реализации честно, по одинаковым проверкам.
Что делают e2e‑тесты:
– поднимают окружение (или подключаются к уже запущенному);
– делают реальные HTTP‑запросы к API;
– проверяют статус-коды, тело ответа, ошибки;
– (опционально) сверяют ответы со схемой OpenAPI.
Как это выглядит на практике:
– вы запускаете api-ts и гоняете e2e – всё зелёное;
– переключаете на api-go и гоняете те же e2e – тесты должны пройти без изменений.
Если тесты завязаны на конкретную реализацию – это плохие e2e.
Хорошие e2e завязаны на контракт.
Минимальные договорённости по именованию и портам
Чтобы всё собиралось без «магии», вводим простые правила:
1. У каждого бэкенда свой порт по умолчанию (например: 3001/3002/3003/3004).
Но порт всегда можно переопределить через переменную окружения.
2. У каждого бэкенда общий базовый префикс API (например, без префикса или /api – главное, чтобы одинаково везде).
3. Фронтенд читает API_BASE_URL из env и не «хардкодит» адрес.
4. E2E тесты читают E2E_BASE_URL (или аналог) – один параметр, чтобы тесты знали, куда стучаться.
Это мелочи, но они экономят огромное количество времени.
0.2.8. Инструменты, которые удобно поставить для работы с монорепо
Ниже – набор программ, которые заметно упрощают жизнь. Ничего “обязательного” здесь нет, но с ними будет легче.
Для работы с Git и кодом
– VS Code – универсальный редактор для всего монорепо.
– IntelliJ IDEA – очень удобна для Spring Boot/Gradle/Maven.
– Git client (по желанию) – если вам проще визуально смотреть историю и конфликты.
Для инфраструктуры
– Docker Desktop (macOS/Windows) или Docker Engine (Linux).
– Postman или Insomnia – быстро вручную дергать API (хотя у нас будет OpenAPI и тесты).
Для работы с API-контрактом
– Плагин OpenAPI для редактора (подсветка, подсказки, валидация).
– Любой HTTP клиент в IDE (в VS Code/JetBrains есть встроенные варианты).
Для языков
– Node.js (лучше через менеджер версий), плюс pnpm.
– Python (через pyenv/uv – как удобнее).
– JDK (например, 21) для JVM-проекта.
– Go (желательно фиксировать версию в проекте).
Что мы получаем в итоге
После того как структура задана, у нас появляется понятная картина:
– В apps/ лежат разные реализации, которые можно запускать по очереди.
– В packages/shared-contract лежит единый контракт API и генерация типов/клиентов.
– В infra/ лежит общая инфраструктура, которую не нужно дублировать.
– В tests/e2e лежат тесты, которые одинаково проверяют любой бэкенд.
Эта структура – фундамент книги. Дальше мы будем добавлять функциональность (эндпоинты, модели, базы данных, авторизацию), но базовые правила останутся прежними: контракт один, тесты одни, реализаций много.
Небольшая правка по ответам из предыдущего раздела
Перед новой главой уточним два момента, чтобы дальше не было путаницы.
Где лежит контракт API и почему он один?
В общем смысле вы правы: контракт – это соглашение. Но в нашем проекте это соглашение должно быть зафиксировано в коде в одном месте, иначе оно начнёт «плавать» между реализациями.
В монорепозитории книги контракт лежит здесь:
– /packages/shared-contract/openapi.yaml
Почему он один:
– единый источник правды: фронтенд, тесты и все бэкенды сверяются с одним файлом;
– нет расхождений «в TS так, а в Go чуть иначе»;
– проще развивать API: изменения проходят через одно место и проверяются автоматически.
Где будут e2e‑тесты и почему они не должны зависеть от конкретного языка?
В нашем проекте e2e‑тесты лежат в:
– /tests/e2e
Они не зависят от языка, потому что их цель – проверять поведение системы через HTTP, то есть:
– одинаковые URL и методы,
– одинаковые статус-коды,
– одинаковые тела ответов,
– одинаковые ошибки.
То, написан сервер на Python или Java, для e2e вообще не важно – важен контракт и фактическое поведение.
Что относится к apps/, а что – к packages/?
– apps/ – запускаемые приложения (frontend и каждый бэкенд).
– packages/ – общие библиотеки/артефакты, которые переиспользуются несколькими приложениями (в нашем случае – контракт и генерация типов/клиентов).
Почему инфраструктура вынесена в infra/, а не разбросана по приложениям?
Потому что инфраструктура у нас общая. Postgres/Redis/очередь – не «часть конкретного api-ts», они нужны всем реализациям.
Если размазать infra по приложениям:
– придётся дублировать docker-compose и конфиги;
– версии сервисов начнут расходиться;
– станет сложнее запускать и поддерживать окружение.
Папка infra/ делает инфраструктуру одной, а значит – повторяемой и предсказуемой.
Контракт как основа: OpenAPI-first
В этой книге мы идём подходом OpenAPI-first: сначала описываем API как контракт, а потом реализуем его на разных языках.
Это дисциплина, которая сначала кажется «лишней бюрократией», но очень быстро окупается:
– фронтенд получает типы и клиент без ручной работы;
– e2e‑тесты знают, что проверять, и могут дополнительно сверять схему;
– разные бэкенды реализуют один и тот же договор, без «творчества»;
– изменения API становятся управляемыми: видно, что сломается.
Наша цель в этой главе простая:
написать openapi.yaml, научиться генерировать из него типы/клиенты и заставить CI проверять, что контракт валиден.
Где живёт OpenAPI в нашем монорепо
Мы договорились: контракт лежит в одном месте.
Структура пакета:
text
/packages/shared-contract
openapi.yaml
/generated
/scripts
– openapi.yaml – главный файл спецификации.
– generated/ – сюда можно складывать сгенерированные артефакты (типы, клиенты).
– scripts/ – скрипты генерации и проверки.
Папки generated и scripts – опциональны, но они помогают держать порядок: «ручной код» отдельно, «генерация» отдельно.
Пишем первый openapi.yaml
Начнём с минимального, но реального API. Нам нужно что-то, что:
1) легко проверить e2e‑тестами;
2) пригодится для health-check;
3) задаст стиль ответов и ошибок.
Сделаем два эндпоинта:
– GET /health – проверка, что сервис жив.
– GET /api/version – показывает версию API (или приложения), чтобы удобно сравнивать окружения.
Создаём файл:
/packages/shared-contract/openapi.yaml
yaml
openapi: 3.0.3
info:
title: Demo API
version: 0.1.0
description: |
Контракт API для учебного проекта.
Все реализации (TS/Python/Java/Go) обязаны ему соответствовать.
servers:
– url: http://localhost:3001
description: Local dev (пример)
tags:
– name: system
description: Системные эндпоинты
paths:
/health:
get:
tags: [system]
summary: Health check
description: Проверяет, что сервис доступен
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "/components/schemas/HealthResponse"
/api/version:
get:
tags: [system]
summary: API version
description: Возвращает версию приложения/контракта
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: "/components/schemas/VersionResponse"
components:
schemas:
HealthResponse:
type: object
additionalProperties: false
required: [status]
properties:
status:
type: string
enum: [ok]
VersionResponse:
type: object
additionalProperties: false
required: [version]
properties:
version:
type: string
example: "0.1.0"
Почему мы сразу добавили additionalProperties: false
Это важная деталь. Она означает: в ответах не должно быть неожиданных полей.
Если это не включать, сервер может вернуть лишнее, фронт случайно начнёт это использовать, а потом вы захотите убрать поле – и получите «тихий» breaking change.
С additionalProperties: false API становится строже, зато стабильнее.
Договоримся о стиле ошибок (на будущее)
Пока в контракте нет ошибок, но уже сейчас полезно задать единый формат. Тогда все реализации будут возвращать одно и то же.
Добавим схему ошибки в components/schemas (даже если эндпоинты пока её не используют):
yaml
components:
schemas:
ApiError:
type: object
additionalProperties: false
required: [code, message]
properties:
code:
type: string
example: "VALIDATION_ERROR"
message:
type: string
example: "Invalid request"
details:
type: object
additionalProperties: true
description: Дополнительная информация (опционально)
Позже мы начнём ссылаться на ApiError в ответах 400/401/404/500. Сейчас важно: мы заранее закрепили формат.
Генерируем клиентов и типы
OpenAPI полезен сам по себе как документация. Но настоящая сила – в генерации.
Что мы обычно хотим генерировать:
– TypeScript типы для фронтенда и e2e‑тестов;
– TypeScript API клиент (чтобы не писать fetch вручную);
– опционально: клиенты для других языков (если нужно).
Какие инструменты можно использовать
Ниже несколько популярных вариантов. В книге можно выбрать один (или даже сменить позже), главное – чтобы процесс был повторяемым.
– OpenAPI Generator – умеет генерировать почти всё, поддерживает массу языков.
– Swagger Codegen – старый, но встречается.
– Для TS:
– генерация типов отдельно (например, из схем),
– генерация клиента (fetch/axios).
Практический совет: для старта проще всего выбрать один генератор, который делает TS‑клиент и типы, и пользоваться им везде.
Куда складывать результат
Есть два подхода:
1) Коммитить generated в репозиторий
Плюсы: быстро, всё видно, CI проще.
Минусы: лишние диффы, больше шума в PR.
2) Не коммитить, генерировать в CI и локально
Плюсы: чище репозиторий.
Минусы: нужно следить, чтобы все запускали генерацию.
Для учебного проекта допустимы оба. Важно не «что лучше в вакууме», а чтобы команда договорилась.
В нашей структуре логично держать генерацию здесь:
– packages/shared-contract/generated/
Как встроить генерацию в рабочий процесс
Нам нужно, чтобы генерация была:
– одинаковой у всех
– воспроизводимой
– не зависела от ручных действий
Минимальная схема работы:
1. Меняем openapi.yaml.
2. Запускаем генерацию (скрипт).
3. Фронтенд и тесты используют обновлённые типы/клиент.
Даже если вы пока не пишете реальные команды, полезно уже сейчас завести «точку входа», например:
– packages/shared-contract/scripts/generate
– packages/shared-contract/scripts/validate
Важно: не размазывать генерацию по разным местам. Контракт и его обслуживание должны жить рядом.
Валидация контракта в CI
Контракт имеет смысл только тогда, когда он валиден и изменения контролируются.
В CI нам нужны проверки:
1) OpenAPI файл корректный YAML
2) OpenAPI соответствует схеме OpenAPI 3.0.x
3) (опционально) в контракте нет грубых проблем (линтинг, стиль, ошибки описания)
Что именно проверять
Минимально достаточно:
– «файл парсится»
– «спецификация валидна»
Чуть лучше:
– запретить дубли, неиспользуемые схемы, неописанные ответы
– требовать operationId на каждом эндпоинте (удобно для генерации клиентов)
Например, можно договориться, что каждый endpoint обязан иметь operationId. Это выглядит так:
yaml
/health:
get:
operationId: getHealth
…
Почему это удобно:
– генератор делает стабильные имена методов;
– проще искать операции в коде и в логах.
Как CI будет это использовать
Идея простая:
– на каждый PR запускается job,
– job валидирует openapi.yaml,
– если файл сломан – PR не проходит.
Так вы избегаете ситуации «спецификация поехала, генерация упала у половины команды».
Правила изменения API (очень коротко)
Чтобы OpenAPI-first реально работал, вводим базовые правила:
1. Любое изменение API начинается с изменения openapi.yaml.
Не наоборот.
2. После изменения контракта должны обновиться генерации (типы/клиенты).
3. Реализации API обновляются после контракта и должны проходить e2e‑тесты.
Это создаёт правильную последовательность: контракт → инструменты → реализация → тесты.
Что будет в следующем разделе
Дальше мы сделаем первый минимальный API на одной из платформ (обычно удобно начать с TypeScript или Python, потому что быстрее старт), и:
– поднимем /health и /api/version так, чтобы они соответствовали OpenAPI;
– подключим e2e‑тесты из /tests/e2e, которые будут проверять эти два эндпоинта;
– подготовим основу, чтобы затем легко переключаться между api-ts, api-py, api-java, api-go.
Главная цель следующего шага: впервые увидеть замкнутый цикл
контракт → реализация → e2e‑тесты – и убедиться, что он работает одинаково для любого языка.
Глава 1. TypeScript/Node – плюсы/минусы (практически)
TypeScript/Node.js – один из самых популярных вариантов для прикладных API: от небольших сервисов до больших BFF и API‑шлюзов. Его часто выбирают не потому, что он «самый быстрый» или «самый строгий», а потому что он быстро даёт результат и хорошо ложится на продуктовую разработку.
В этой главе разберём плюсы и минусы именно практично: что вы почувствуете в проекте через неделю, месяц и год. Без теории «что такое event loop», а с фокусом на ежедневные решения: скорость разработки, качество типов, предсказуемость под нагрузкой и дисциплина архитектуры.
1.1. Что мы подразумеваем под TypeScript/Node.js в книге
Чтобы говорить предметно, зафиксируем контекст. Когда дальше в книге будет «TS/Node.js реализация», мы обычно имеем в виду:
– Node.js как runtime (чаще LTS‑версия).
– TypeScript как язык разработки.
– Веб‑фреймворк уровня Express/Fastify/Nest (выбор влияет на стиль, но не отменяет общие свойства платформы).
– Обязательное наличие:
– линтера (ESLint),
– форматтера (Prettier),
– тестов,
– сборки (tsc, иногда bundler),
– строгих настроек TypeScript (насколько возможно).
Что полезно установить для работы
Ниже – типичный минимум. Конкретные версии не важны, важна идея:
– Node.js (LTS) – среда выполнения.
– npm / pnpm / yarn – менеджер пакетов (любое одно).
– TypeScript – компилятор и типизация.
– Docker – удобно для инфраструктуры (PostgreSQL/Redis/очереди), чтобы окружение было одинаковым у всех.
– HTTP‑клиент для ручной проверки: curl или Postman/Insomnia.
– Инструмент для профилирования (на будущее): встроенный Node inspector и/или клинические утилиты типа autocannon для нагрузочного прогона.
Это не «обязательный список для счастья», но с ним меньше сюрпризов.
1.2. Плюсы: почему TS/Node.js часто выигрывает в реальности
Плюс 1. Максимальная скорость разработки и огромная экосистема
На практике Node.js выигрывает там, где важны:
– быстро поднять сервис;
– быстро интегрироваться со сторонними системами;
– быстро менять продуктовую логику.
Причина проста: экосистема.
Для большинства задач уже есть готовые решения:
– авторизация (JWT, OAuth),
– валидация входных данных,
– логирование, трассировка,
– очереди, кэш, базы данных,
– платежи, интеграции, SDK внешних сервисов,
– генерация клиента из OpenAPI.
Это снижает «время до первой работающей версии» и уменьшает риск, что вам придётся писать инфраструктурные куски самим.
Что вы ощущаете в проекте:
– меньше времени уходит на «поднять каркас»;
– много вещей можно сделать через конфигурацию;
– проще нанять разработчиков: Node/TS распространены.
Важное уточнение: экосистема – это и плюс, и источник риска. Пакетов много, качество разное. Поэтому дисциплина выбора зависимостей – отдельная тема (вернёмся к ней ниже в минусах).
Плюс 2. Один язык на фронт и бэк – удобно для full‑stack
Когда фронтенд и бэкенд на одном языке, появляется набор «мелких», но очень ощутимых преимуществ:
– общие принципы типизации и структуры данных;
– один стиль работы с JSON и датами;
– проще «перекидывать» людей между задачами;
– проще писать BFF (Backend For Frontend), где API подстраивается под UI.
Если команда в основном из фронтендеров, TypeScript‑бэкенд – это самый короткий путь:
– не нужно учить Java/Go «с нуля»,
– можно переносить привычные практики (линтинг, форматирование, подход к модулям).
Особенно хорошо этот плюс раскрывается, когда у вас:
– много экранов и фич,
– API постоянно меняется,
– продукт ещё ищет «правильную форму».
Плюс 3. TypeScript даёт контракты, автокомплит и рефакторинг
TypeScript – это не «магическая защита от багов», но это мощный инструмент, который:
– делает структуры данных явными;
– помогает IDE подсказывать правильные поля и типы;
– позволяет рефакторить безопаснее (переименования, перемещения, выделения типов).
На практике вы быстро замечаете две вещи:
1) Код становится самодокументируемым.
Хорошо описанные типы входа/выхода читаются как документация.
2) Меньше «неожиданных» ошибок на ровном месте.
Например, когда поле называется `createdAt`, а вы случайно использовали `createAt`.
Но есть тонкость: типы в TS особенно хороши, когда вы:
– избегаете `any`,
– ограничиваете «непроверенные» данные на границе (HTTP запросы, внешние API),
– используете строгие настройки компилятора.
Иначе TypeScript может превратиться в видимость безопасности.
Плюс 4. Отлично для BFF, API‑шлюзов и SaaS
Есть классы задач, где Node.js чувствует себя «дома»:
– BFF: собрать данные из нескольких источников, подготовить JSON под конкретный UI.
– API‑шлюз: проксирование, авторизация, rate limit, агрегация.
– SaaS‑продукты: много бизнес‑логики, много интеграций, постоянные изменения.
Node.js особенно силён в I/O‑нагрузке:
– много запросов,
– много походов в базу/кэш/внешние сервисы,
– много «склеивания» ответов.
Здесь важнее не «сырой CPU», а скорость разработки и удобство интеграций.
1.3. Минусы: где Node.js может ударить больно
Минусы – не «приговор». Это просто список мест, где нужно осознанно компенсировать слабые стороны платформы.
Минус 1. Производительность и предсказуемость latency обычно хуже, чем у Go/Java
В среднем, при равной реализации и при нагрузке, Go и Java часто дают:
– более высокую пропускную способность,
– более стабильные хвостовые задержки (p95/p99),
– лучшее использование CPU.
Node.js может показывать отличные результаты, но есть типичные причины, почему latency «плывёт»:
– один event loop: если вы случайно сделали тяжёлую синхронную работу, она блокирует обработку запросов;
– сборка мусора (GC): паузы могут проявляться как редкие, но неприятные пики;
– зависимости: одна «неудачная» библиотека может создать нагрузку и ухудшить хвосты.
Что это значит практично:
– для большинства продуктовых API это не проблема на старте;
– но при росте нагрузки и требований к p99 придётся:
– профилировать,
– контролировать память,
– оптимизировать горячие места,
– иногда выносить CPU‑тяжёлое в отдельные воркеры/сервисы.
Если у вас система, где критичны микросекунды/миллисекунды и стабильность p99 (например, высокочастотный трейдинг), Node.js будет сложнее «довести» до уровня Go/Java.
Минус 2. “Железобетонная” типобезопасность сложнее, чем в Java
TypeScript – язык со статической типизацией, но он остаётся «надстройкой» над JavaScript. Это проявляется в трёх местах:
1) Границы системы
Всё, что пришло извне (HTTP запрос, сообщение из очереди, ответ другого сервиса), по-настоящему имеет тип `unknown`.
Если вы не валидируете входные данные, типы становятся самообманом.
2) Лазейки типизации
`any`, нестрогие настройки компилятора, приведения типов ради скорости – и вот типы уже не защищают.
3) Сложные типы могут стать “типовой магией”
TypeScript позволяет строить очень мощные типовые конструкции, но иногда это превращает код в ребус:
– сложно читать,
– сложно дебажить,
– сложно объяснять новичкам.
Практический вывод: в TS безопасность типов достигается не «по умолчанию», а дисциплиной:
– строгий `tsconfig`,
– минимум `any`,
– валидация входов (схемы),
– генерация типов из контракта (OpenAPI) вместо ручного описания.
Минус 3. Память и GC под нагрузкой требуют аккуратности
В Node.js легко не заметить, как сервис начинает потреблять слишком много памяти:
– большие JSON‑ответы;
– лишние копии объектов;
– хранение данных в кэше процесса без ограничений;
– утечки через глобальные структуры;
– слишком «жирные» зависимости.
А потом наступает момент, когда:
– контейнер перезапускается по OOM,
– GC начинает чаще работать,
– latency становится зубчатым.
Практически это решается, но нужно:
– следить за memory usage,
– уметь делать heap snapshot,
– ограничивать кэши и буферы,
– понимать жизненный цикл объектов.
Это не означает, что Node «плохой». Это означает, что под нагрузкой вам понадобится инженерная внимательность.
Минус 4. Нужна дисциплина архитектуры – иначе проект “расползётся”
Node.js и TypeScript дают большую свободу. Это хорошо, пока проект маленький. Но свобода быстро превращается в хаос, если нет правил:
– где лежит бизнес‑логика,
– где слой доступа к данным,
– как устроены модули,
– как организованы DTO/схемы,
– как оформляются ошибки,
– как пишутся тесты.
Симптомы «расползания» обычно такие:
– контроллеры по 300 строк;
– бизнес‑логика размазана по роутам;
– разные форматы ошибок в разных местах;
– отсутствие границ между слоями;
– типы начинают дублироваться и расходиться.
Это решается не «правильным фреймворком», а правилами и привычками:
– единый стиль проекта,
– единые контракты,
– генерация из OpenAPI,
– архитектурные границы.
Если дисциплины нет, TS/Node проект может деградировать быстрее, чем аналогичный на более «тяжёлых» платформах, где часть структуры навязывается инструментами.
1.4. Когда выбирать TypeScript/Node.js
Ниже – ситуации, когда выбор TS/Node обычно оправдан и даёт максимальную отдачу.
Сценарий 1. Быстрое MVP → продукт → масштабирование с профилированием
Самый типичный путь:
1) Вы делаете MVP быстро: больше ценности, меньше церемоний.
2) Продукт начинает расти: добавляются фичи, интеграции, команды.
3) Появляется нагрузка: вы профилируете, оптимизируете, усиливаете наблюдаемость.
4) Если нужно, выносите горячие места:
– в отдельные воркеры,
– в отдельные сервисы (на Go/Java, если действительно требуется).
Node.js отлично подходит как «двигатель продукта», где скорость изменений важнее абсолютной эффективности.
Сценарий 2. Команда фронтендеров, которым нужен бэк
Если у вас сильная фронтенд‑команда и нужно быстро закрыть бэкенд‑потребности:
– TypeScript снижает порог входа;
– общие подходы к типам и контрактам упрощают коммуникацию;
– проще поддерживать BFF и API под нужды UI.
Обычно в таких командах успех зависит от двух вещей:
– контракт (OpenAPI-first, как мы договорились);
– архитектурные правила (иначе всё уйдёт в «быстрее бы работало»).
Сценарий 3. BFF/API‑шлюз как отдельный слой
Если ваш бэкенд – это в основном:
– агрегация,
– маршрутизация,
– преобразование данных,
– авторизация и ограничения,
то Node.js – сильный кандидат, потому что:
– I/O‑операции – его естественная среда,
– экосистема даёт массу готовых компонентов,
– разработка и поддержка быстрее.
1.5. Практические рекомендации, чтобы плюсы не превратились в минусы
Эта часть короткая, но очень прикладная: что стоит сделать почти в любом TS/Node API проекте, чтобы жить спокойнее.
1) Делайте строгий TypeScript “по умолчанию”
Смысл: пусть компилятор «ворчит», пока проект маленький. Это дешевле, чем переписывать позже.
– включайте строгие проверки (`strict` и связанные флаги);
– минимизируйте `any`;
– работайте с внешними данными как с `unknown` и валидируйте их.
2) Валидируйте входы и выходы на границе
Типы внутри кода – хорошо. Но запрос из интернета не становится типом автоматически.
– входные данные: валидируем (схемы/валидаторы);
– выходные данные: следим, чтобы соответствовали контракту.
Это особенно важно, если вы хотите, чтобы разные реализации (TS/Python/Go/Java) вели себя одинаково.
3) Следите за размером зависимостей и качеством пакетов
Экосистема огромная, но это не значит, что любую библиотеку стоит тянуть в проект.
Полезные привычки:
– не добавлять зависимость «ради одной функции»;
– смотреть на поддержку и актуальность;
– обновлять регулярно, а не раз в год «одним большим взрывом».
4) Добавьте наблюдаемость до того, как станет больно
Минимум, который окупается рано:
– структурированные логи,
– корреляционный id,
– метрики (хотя бы время ответа и ошибки),
– трассировка (по возможности).
Так вы быстрее поймёте, где Node «упёрся» – в базу, в внешние API или в CPU.
5) Держите архитектуру простой, но с границами
Не обязательно усложнять. Но границы должны быть:
– transport слой (HTTP) отдельно,
– бизнес‑логика отдельно,
– доступ к данным отдельно,
– общие типы/контракт отдельно.
Это снижает «расползание» и облегчает тестирование.
1.6. Итог
TypeScript/Node.js – выбор про скорость и удобство:
– быстро разрабатывать,
– легко интегрироваться,
– удобно жить в одном языке с фронтендом,
– отлично подходит для BFF и продуктовых API.
Но за это платите необходимостью инженерной дисциплины:
– следить за типовой безопасностью на границах,
– контролировать память и GC под нагрузкой,
– профилировать и работать с latency,
– не давать архитектуре расползаться.
Если вы хотите быстро выйти на рынок, постоянно менять продукт и у вас сильная фронтенд‑команда – TS/Node почти всегда хороший старт. А дальше вы либо продолжите масштабироваться на Node с профилированием, либо точечно вынесете критичные части туда, где лучше предсказуемость и производительность.