Читать книгу «Full stack Developer» онлайн полностью📖 — Юрий Белк — MyBook.

– сравнивать реализации сложнее – всё раскидано по разным местам.

Монорепо решает это простым способом: всё рядом. В результате вы быстрее видите, чем отличаются подходы, а не боретесь с организацией.

Общая структура проекта

Мы используем фиксированную структуру папок. Она будет одинаковой на протяжении всей книги:

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