Читать книгу 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 с профилированием, либо точечно вынесете критичные части туда, где лучше предсказуемость и производительность.

Full stack Developer

Подняться наверх