Юрий Белк
Full stack Developer

Об чём речь?

Книга‑туториал (максимально практическая): Full‑Stack + Backend Engineering на TS / Python / Java / Go

Сравнение языков, пошаговые проекты, одна предметная область, одинаковые требования.

Чтобы вы могли писать один и тот же продукт четырьмя реализациями бэкенда (TS/Python/Java/Go) и одной фронтенд‑частью, а затем сравнивать: скорость разработки, качество, тестируемость, производительность, сложность деплоя, типизацию, экосистему.

Как устроена книга

Главная идея

Мы строим один и тот же продукт (например, TaskFlow – сервис задач/проектов/команд):

Frontend: TypeScript + React/Next.js (единый для всех)

Backend: 4 реализации одного API:

1) Node.js + TypeScript (например, NestJS/Fastify)

2) Python (FastAPI)

3) Java (Spring Boot)

4) Go (Gin/Fiber/chi)

DB: PostgreSQL

Очереди: (опционально) RabbitMQ/NATS/Kafka (раздел сравнения)

Кэш: Redis

Observability: OpenTelemetry + Prometheus + Grafana + Loki

Infra: Docker Compose → CI/CD → Kubernetes (опционально)

На что будет опираться каждая реализация

Единая OpenAPI спецификация (контракт)

Единая схема БД и миграции

Единые acceptance tests (e2e) для всех реализаций

Единый набор сценариев нагрузки (k6/Locust/JMeter)

Для кого?

От “почти ноль” до уровня уверенного инженера

Для тех, кто хочет практику, но при этом понимать компромиссы

Стандартная структура каждой главы (шаблон)

Каждая глава оформляется одинаково:

1. Цель и результат (что получится в конце)

2. Предварительные требования

3. Шаги (команды + код)

4. Проверка результата (что увидеть/какие тесты проходят)

5. Типовые ошибки и дебаг

6. Домашка/усиление

7. Сравнение TS vs Python vs Java vs Go (если применимо)

Раздел I. Подготовка: инструменты, репо, “скелет книги”

Этот раздел нужен, чтобы один раз настроить окружение и дальше спокойно проходить всю книгу. Мы будем сравнивать несколько бэкендов (TypeScript, Python, JVM, Go) и один фронтенд, поэтому важно сразу договориться о правилах: как ставим зависимости, как запускаем сервисы, где живут контракты API, и как устроен репозиторий.

Главная идея: один монорепозиторий, один docker-compose для инфраструктуры, один контракт OpenAPI, и единые e2e‑тесты, которые одинаково проверяют любой бэкенд.

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

Глава 2. Python – плюсы/минусы

Python часто выбирают не из‑за «идеальной архитектуры» «самой высокой производ», а из‑за очень практичной вещи: на нём быстрее всего превращать идею в работающий код. Это язык, на котором одинаково комфортно написать API, скрипт мигра данных, интеграцию со сторонним сервисом, джобу для очереди и небольшой ML‑пайплайн.

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

В этой главе разберём Python «по‑земному»: где он особенно хорош, где может подставить и как понять, подходит ли он под ваш сервис.

2.1. Что мы подразумеваем под Python‑бэкендом

Под «Python‑бэкендом» в этой книге обычно подразумевается:

– Python 3.x (желательно актуальный, не «как на сервере поставили 5 лет назад»).

– Web API на одном из фреймворков:

– FastAPI (часто лучший баланс для современных API),

– Django (если нужен «комбайн» с ORM, админкой и большим количеством встроенных решений),

– Flask (минималистичный вариант).

– Работа в контейнерах (часто Docker), чтобы окружение было повторяемым.

– Тесты и линтинг как обязательная часть проекта.

Что стоит установить для работы

Минимальный набор, который обычно облегчает жизнь:

– Python 3 (лучше ставить через менеджер версий вроде `pyenv`, если вы на macOS/Linux).

– pip (идёт вместе с Python) + менеджер зависимостей:

– `poetry` или `uv` (быстро и удобно),

– либо классический `pip` + `requirements.txt` (простая модель, но требует дисциплины).

– Docker (PostgreSQL/Redis/очереди удобно поднимать контейнерами).

– Postman/Insomnia или просто `curl` для ручной проверки API.

– Инструменты качества кода:

– форматирование: `ruff format` или `black`,

– линтинг: `ruff`,

– типизация: `mypy` или `pyright`,

– тесты: `pytest`.

Эти инструменты не «украшают» проект – они компенсируют те места, где Python по умолчанию менее строгий.

2.2. Плюсы Python

Плюс 1. Самая высокая скорость написания бизнес‑логики

Python ценят за то, что он позволяет писать много полезного кода с минимумом шума. Это особенно заметно в бизнес‑логике, где часто нужно:

– обработать входные данные;

– сходить в БД/кэш/внешний сервис;

– принять решение по правилам;

– вернуть результат.

В Python вы редко тратите время на «обвязку». Код получается коротким, читаемым и легко изменяемым.

Почему так выходит на практике:

– синтаксис простой и близкий к «псевдокоду»;

– много встроенных возможностей для работы со строками, коллекциями, датами;

– богатая стандартная библиотека;

– огромный выбор готовых библиотек под почти любую задачу.

Для продуктовой разработки это важно. Когда требования меняются каждую неделю, скорость внесения правок иногда важнее, чем идеальная модель типов или максимальная производительность.

Что вы почувствуете в проекте:

– быстро появляется «вертикальный срез» фичи (от API до результата);

– проще экспериментировать: менять правила, добавлять поля, перестраивать процесс;

– легче писать небольшие утилиты вокруг сервиса (например, скрипт импорта или диагностики).

Плюс 2. Отличен для интеграций, автоматизаций, data/ML

Python – фактически стандарт для задач, где нужно работать с данными и автоматизировать процессы:

– обработка файлов (CSV/Excel/JSON);

– интеграции с API внешних систем;

– фоновые задачи (выгрузки, перерасчёты, рассылки);

– пайплайны данных;

– аналитика и машинное обучение.

Важно, что это всё часто встречается не только в «data‑командах». Обычный продуктовый бэкенд регулярно сталкивается с задачами типа:

– загрузить прайс от партнёра;

– сопоставить сущности (матчинг);

– рассчитать метрики и сохранить агрегаты;

– прогнать правила качества данных;

– подготовить датасет для модели;

– интегрироваться с CRM, платёжкой, складом.

В Python‑мире для этого почти всегда есть зрелые библиотеки. Часто они появляются раньше и развиваются быстрее, чем аналоги в других экосистемах.

Практический эффект:

– меньше «велосипедов»;

– проще собирать конвейеры из готовых частей;

– удобнее поддерживать единую кодовую базу, где рядом живут API и data‑задачи.

Плюс 3. FastAPI даёт прекрасный DX и OpenAPI из коробки

FastAPI стал популярным по очень понятным причинам:

– он быстрый в разработке;

– поощряет типизацию (через type hints);

– автоматически генерирует OpenAPI‑схему;

– даёт интерактивную документацию (обычно Swagger UI) почти бесплатно.

DX (developer experience) здесь реально чувствуется:

– вы описали модель входа/выхода – и документация уже обновилась;

– клиентским командам проще тестировать API;

– меньше времени уходит на «а как этим пользоваться».

Кроме того, FastAPI подталкивает к более аккуратной структуре:

– явные модели запросов/ответов;

– валидация данных через схемы;

– понятная работа с зависимостями (dependency injection).

Важно понимать нюанс: FastAPI даёт много «из коробки», но архитектуру всё равно придётся продумывать. Иначе проект так же легко превратится в набор эндпоинтов с логикой внутри.

Плюс 4. Низкий порог входа и широкая доступность разработчиков

Python часто выбирают ещё и потому, что:

– его знают многие;

– на нём проще онбордить людей;

– он хорош для команд, где есть не только классические бэкендеры, но и аналитики/инженеры данных/автоматизаторы.

Если вы строите продукт, в котором бэкенд тесно связан с аналитикой или обработкой данных, Python помогает уменьшить «стык культур»: меньше барьеров между командами и меньше потерь при передаче контекста.

2.3. Минусы Python

Минус 1. Типизация слабее (mypy/pyright помогают, но не как Java/TS)

Python поддерживает type hints, и это большой шаг вперёд по сравнению с прошлым. Но важно честно признать: по строгости и «железобетонности» это обычно уступает языкам, где типизация – часть ядра (Java, Kotlin, TypeScript в строгом режиме).

В чём проблема на практике:

1) Типы не обязательны.

Вы можете не писать их вовсе – и код продолжит работать. Это означает, что типовая дисциплина держится на договорённостях и проверках в CI.

2) Типы не влияют на рантайм автоматически.

Даже если вы всё типизировали, на проде Python всё равно выполнит код, даже если вы передали «не то». Типы ловят ошибки на стадии анализа, но не защищают в рантайме сами по себе.

3) Сложные случаи типизации могут быть неудобными.

Дженерики, сложные объединения типов, протоколы, ковариантность – всё это есть, но часто воспринимается как «отдельная наука», и команда начинает избегать типизации в сложных местах.

mypy и pyright реально помогают:

– ловят ошибки в рефакторингах;

– улучшают автокомплит;

– повышают уверенность при изменениях.

Но это работает, только если:

– вы включили проверки и не отключаете их «ради скорости»;

– вы используете типы последовательно;

– вы аккуратно работаете с границами (входные данные из внешнего мира).

Практический вывод: типизация в Python – это инструмент качества, который нужно сознательно включать и поддерживать, иначе он «растворяется» в проекте.

Минус 2. Производительность часто ниже; async требует дисциплины

Python обычно медленнее по CPU‑задачам, чем Go/Java/Node (V8 часто быстрее в чистых вычислениях). В прикладном API это не всегда критично, потому что большинство запросов – это I/O:

– база данных;

– кэш;

– внешний сервис.

Но проблемы начинаются, когда:

– в запросе много преобразований данных;

– вы сериализуете/десериализуете большие объёмы JSON;

– вы делаете тяжёлые вычисления в обработчике запроса;

– вы случайно блокируете event loop (в async‑варианте).

Почему «async требует дисциплины»

FastAPI и современный Python‑стек часто используют `async`/`await`. Это мощно, но легко ошибиться:

– вы используете синхронную библиотеку внутри `async`‑эндпоинта;

– вы делаете блокирующий вызов (например, обычный HTTP‑клиент без async);

– вы запускаете CPU‑тяжёлое в том же потоке;

– вы не контролируете время ожидания и ретраи.

Снаружи это проявляется как странные симптомы:

– сервис «живой», но отвечает медленно;

– под нагрузкой резко растут задержки;

– метрики показывают, что CPU не загружен на 100%, но запросы висят.

Проблема не в том, что async «плохой». Проблема в том, что в Python легко смешать async и sync так, что вы сами себе создаёте пробки.

Обычно помогают практики:

– чётко выбирать: этот сервис в основном async или в основном sync;

– использовать подходящие библиотеки (async‑драйверы БД, async‑HTTP клиент);

– выносить CPU‑тяжёлое в фоновые воркеры;

– ограничивать параллелизм и ставить таймауты.

Минус 3. Параллелизм сложнее (GIL), обычно уходят в процессы/очереди

В Python есть известная особенность: GIL (Global Interpreter Lock) в CPython. Упрощённо: в одном процессе Python‑код не исполняется параллельно на нескольких ядрах так, как вы могли бы ожидать от потоков.

Что это значит для бэкенда:

– Для I/O‑нагрузки это часто не критично: пока вы ждёте сеть/БД, можно обрабатывать другие запросы.

– Для CPU‑нагрузки это становится проблемой: если у вас тяжёлая обработка данных, один процесс будет упираться в одно ядро.

Типичные решения в продакшене:

1) Масштабирование процессами

Запускают несколько воркеров веб‑сервера (несколько процессов). Это даёт использование нескольких ядер.

2) Очереди и фоновые задачи

CPU‑тяжёлое выносят из HTTP‑обработчика в отдельные воркеры: так API остаётся быстрым, а тяжёлая работа выполняется асинхронно.

3) Отдельные сервисы для тяжёлых расчётов

Иногда проще вынести вычисления в отдельный компонент (на другом языке или в отдельной инфраструктуре), чем бороться с ограничениями внутри одного API.

4) Нативные расширения

Для некоторых задач используют библиотеки, которые внутри реализованы на C/C++/Rust и обходят ограничения, потому что тяжёлая часть выполняется вне интерпретатора.

Практическая мысль: Python отлично работает как «клей» и как слой бизнес‑логики, но если ваш сервис – это постоянные тяжёлые вычисления на запрос, вам почти наверняка понадобится отдельная стратегия параллелизма.

Минус 4. Управление зависимостями и окружением может быть источником боли

Это не уникально для Python, но в Python это встречается чаще из‑за большого количества способов «как правильно» управлять пакетами.

В реальных проектах проблемы выглядят так:

– «у меня локально работает, в CI нет»;

– «после обновления зависимости всё сломалось»;

– «на сервере другая версия Python»;

– «библиотека тянет несовместимые версии зависимостей».

Эта боль сильно уменьшается, если:

– фиксировать версии зависимостей;

– использовать виртуальные окружения;

– контейнеризировать приложение;

– иметь понятный способ сборки (один, а не три разных в разных командах).

2.4. Когда выбирать Python

Сценарий 1. Продукты с аналитикой/ML и плотной работой с данными

Если в продукте важны:

– рекомендации;

– скоринг;

– сегментации пользователей;

– обработка событий и метрик;

– сложные расчёты и подготовка данных;

то Python часто становится естественным выбором, потому что:

– ML/аналитический стек живёт в Python‑мире;

– проще делить код и знания между data‑частью и API‑частью;

– проще быстро проверять гипотезы и переносить их в сервис.

Здесь важно трезво оценить архитектуру:

– не обязательно выполнять тяжёлые вычисления внутри API‑запроса;

– но удобно, когда API и подготовка данных рядом и используют один язык.

Сценарий 2. Интеграции и автоматизация

Python особенно хорош, когда ваш сервис:

– постоянно общается со сторонними API;

– обрабатывает файлы и выгрузки;

– выполняет фоновые задания;

– нужен как «интеграционный слой» между системами.

Причина проста: писать такие вещи на Python быстро и удобно, а библиотеки под интеграции чаще всего уже есть.

Сценарий 3. Быстрые API, где важна скорость изменений

Если вы строите продукт, где:

– API часто меняется;

– бизнес‑правила уточняются по ходу;

– важно быстро выпускать фичи;

то Python (особенно с FastAPI) может дать отличный темп разработки.

Ограничение здесь одно: когда нагрузка вырастет, может понадобиться:

– профилирование;

– оптимизация;

– вынос тяжёлых частей в фоновые задачи;

– масштабирование горизонтально.

Обычно это нормальная цена за быстрый старт.

Сценарий 4. Когда критично наличие библиотек Python‑мира

Иногда выбор делается очень просто: «нам нужна вот эта библиотека / SDK / стек, и он нормально живёт в Python».

Это может быть:

– библиотека для обработки документов/медиа;

– инструменты для NLP;

– специфические форматы данных;

– SDK вендора, который лучше всего поддерживается именно в Python.

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

2.5. Практические рекомендации, чтобы Python‑сервис был надёжным

Ниже – список привычек, которые особенно полезны для Python‑бэкенда. Это не «идеальный стандарт», а вещи, которые чаще всего окупаются.

1) Зафиксируйте версии Python и зависимостей

– выберите версию Python и закрепите её (в документации проекта и в сборке);

– фиксируйте зависимости, чтобы сборка была повторяемой;

– обновляйте зависимости регулярно небольшими шагами, а не раз в год.

2) Включите линтинг, форматирование и тип‑чек в CI

Минимальный набор:

– форматирование (автоматическое);

– линтинг (ошибки стиля, небезопасные конструкции);

– проверка типов (mypy/pyright);

– тесты.

Идея простая: пусть качество кода проверяет конвейер, а не память разработчиков.

3) Явно отделяйте границы: входные данные валидируйте

Даже если вы используете type hints, входные данные из сети не становятся «правильными» сами.

Сильная практика для API:

– валидировать запросы схемами;

– возвращать ответы в согласованном формате;

– не смешивать внутри обработчика: «парсинг запроса», «бизнес‑логика», «работа с БД».

FastAPI здесь помогает, но всё равно важно держать границы осознанно.

4) Аккуратно выбирайте модель конкурентности

Простой ориентир:

– Если сервис в основном про I/O и API – можно идти в async, но использовать только совместимые async‑библиотеки и следить за блокирующими местами.

– Если сервис простой и команда не хочет усложнять – иногда лучше оставить sync‑подход и масштабироваться воркерами/процессами.

– CPU‑тяжёлое – почти всегда выносить из HTTP‑пути: в фоновые воркеры, очереди или отдельный сервис.

5) Заранее добавьте наблюдаемость

Минимум, который помогает отлавливать проблемы:

– структурированные логи (чтобы их можно было искать);

– идентификатор запроса (request id);

– метрики времени ответа и ошибок;

– таймауты и ретраи для внешних вызовов.

Python‑сервисы часто страдают не от «падений», а от тихих деградаций производительности. Наблюдаемость помогает заметить это раньше пользователей.

2.6. Итог

Python – очень сильный выбор, когда вам нужна скорость разработки и богатая экосистема:

– быстрее всего писать бизнес‑логику;

– отлично подходит для интеграций, автоматизаций, data/ML‑задач;

– FastAPI даёт удобную разработку и OpenAPI практически без усилий.

Но у Python есть ограничения, о которых нужно помнить:

– типизация слабее и держится на дисциплине и инструментах;

– производительность часто ниже, а async‑подход требует аккуратности;

– параллелизм для CPU‑нагрузки сложнее из‑за GIL – обычно спасаются процессами, очередями или выносом тяжёлого из HTTP.

Если ваш продукт живёт на стыке API и данных, если важны библиотеки Python‑мира и нужно быстро двигаться – Python будет одним из самых практичных вариантов. Если же у вас жёсткие требования к хвостовым задержкам, очень высокая нагрузка и много CPU‑работы на запрос – Python тоже возможен, но архитектуру придётся продумывать особенно тщательно.

Глава 3. Java – плюсы/минусы

Java – это язык, который редко выбирают «потому что модно». Его выбирают, когда нужно, чтобы система жила долго, предсказуемо и под нагрузкой, а команда могла спокойно развивать её годами, не превращая каждый релиз в прыжок веры.

Если Python – это «быстро сделать правильно (и иногда чуть-чуть надеяться)», то Java – «сделать основательно, чтобы не дрожало». Иногда это звучит скучно. Но скучно – это часто хорошо, если речь о платежах, кредитах и миллионах пользователей.

3.1. Что обычно значит «Java-бэкенд»

Под Java-бэкендом в реальных компаниях чаще всего подразумевается:

– Java 17+ (или хотя бы 11+, но лучше не застревать в прошлом).

– Spring Boot как главный фреймворк.

– База данных (часто PostgreSQL/MySQL), кеш (Redis), брокер сообщений (Kafka/RabbitMQ).

– ORM (чаще всего Hibernate/JPA) или работа через SQL/DSL.

– Сборка через Maven или Gradle.

– Наблюдаемость: метрики, логи, трейсинг.

Java-сервис обычно выглядит «толще» по инфраструктуре и конфигурации, чем Python/Go. Но зато многие вещи стандартизированы: новый инженер приходит – и узнаёт половину инструментов с первого дня.

3.2. Что поставить для работы

Если вы начинаете с Java, установите (минимум):

– JDK (лучше LTS: 17 или 21).

– IDE: IntelliJ IDEA (очень помогает именно в Java).

– Gradle или Maven (скорее всего потребуется один из них).

– Docker (поднимать БД/Redis/Kafka локально).

– Клиент для БД (например, DBeaver или DataGrip).

– Инструменты диагностики:

– JFR (Java Flight Recorder) – встроенный «чёрный ящик» для профилирования,

– jcmd/jstack/jmap – базовые утилиты JDK.

Для проекта также почти всегда нужны:

– тесты (JUnit 5),

– статанализ (Checkstyle/SpotBugs/PMD – по вкусу команды),

– форматирование (например, Spotless),

– линтеры и проверки в CI.

3.3. Плюсы Java

Плюс 1. Предсказуемость, зрелость и «enterprise-паттерны»

Java – это язык, который десятилетиями обкатывали на больших системах. Это чувствуется:

– архитектурные подходы хорошо описаны;

– типовые решения повторяемы;

– много готовых практик для масштабных кодовых баз.

В Java легче строить систему, которая:

– переживёт смену команды;

– выдержит много лет разработки;

– сохранит читаемость при росте количества модулей и интеграций.

Важно: зрелость Java – не про «старомодно», а про «проверено на сотнях похожих систем».

Плюс 2. Высокая производительность и хорошая многопоточность

Java – один из самых сильных «универсальных» языков по производительности в продакшене.

Почему:

– JVM умеет оптимизировать горячие участки кода (JIT-компиляция),

– есть развитые модели параллельного выполнения,

– хорошие библиотеки для конкурентности (пулы потоков, Future/CompletableFuture и т.д.),

– зрелые GC-алгоритмы.

На практике это означает:

– Java-сервис может выдерживать большую нагрузку без экзотики;

– проще «выжимать» ресурсы из железа;

– понятнее поведение при росте throughput.

Отдельный плюс – многопоточность. Да, в ней можно ошибиться (как и везде), но инструменты и паттерны для неё в Java зрелые.

Если у вас есть сервис, где:

– много запросов,

– много параллельных операций,

– много взаимодействий с разными системами,

Java обычно чувствует себя уверенно.

Плюс 3. Spring ecosystem, observability и tooling – действительно топ

Spring Boot – это по сути «операционная система для сервиса». Он даёт:

– быстрый старт приложения,

– DI (dependency injection) как основу архитектуры,

– удобную конфигурацию,

– интеграции почти со всем: БД, брокеры, кэш, security, миграции.

И что особенно важно для зрелых систем:

наблюдаемость и диагностика в Java-мире развиты очень сильно.

Когда сервис работает в проде, вам важно:

– видеть метрики (RPS, latency, ошибки, GC, пул потоков, соединения к БД),

– быстро находить узкие места,

– понимать, где время тратится «на самом деле».

В Java это обычно решается стандартным набором инструментов, и многим инженерам они знакомы.

Если упростить: Java не только помогает «написать код», но и помогает эксплуатировать сервис годами.

Плюс 4. Строгая типизация – архитектура держится лучше

Java заставляет описывать вещи явно:

– модели данных,

– интерфейсы,

– контракты между слоями.

Это добавляет кода. Но зато:

– рефакторинг безопаснее,

– IDE помогает сильнее,

– меньше ошибок «ой, там пришло не то поле»,

– проще поддерживать большой проект.

Типизация особенно помогает, когда:

– много разработчиков,

– много модулей,

– много интеграций,

– требования часто меняются.

Java – это про то, чтобы изменения были управляемыми, а не героическими.

3.4. Минусы Java

Минус 1. Больше «церемоний» и выше порог входа

Java-код часто получается объёмным. Даже простая вещь может требовать:

– классы,

– интерфейсы,

– DTO,

– конфиги,

– аннотации,

– зависимости.

Поначалу это утомляет. Иногда возникает ощущение, что вы не пишете программу – вы заполняете документы. (Да, это тот самый «enterprise-стиль», у которого есть и плюсы, и побочные эффекты.)

Новичкам бывает сложно, потому что нужно понять сразу много слоёв:

– как устроен Spring,

– как работает DI,

– что такое контекст приложения,

– как конфигурируются бины,

– как работает транзакционность.

И это ещё до того, как вы написали бизнес-логику.

Минус 2. Скорость прототипирования ниже

Java умеет быстро, когда у вас есть шаблоны, генераторы, готовые модули.

Но «в лоб», с нуля – прототипирование чаще медленнее, чем в Python/Node.

Причины:

– больше кода и структуры,

– нужно заранее думать о типах и моделях,

– больше времени на настройку проекта.

Если задача звучит так:

«Нам нужно за 2 дня поднять API и проверить гипотезу» – Java может быть не самым лёгким вариантом.

Хотя, если у вашей команды уже есть готовые скелеты проектов и привычный стек, разрыв уменьшается.

Минус 3. DevEx иногда сложнее (особенно новичкам)

В Java DevEx в целом сильный, но не всегда дружелюбный к новичку:

– много магии аннотаций Spring;

– ошибки конфигурации могут быть длинными и пугающими;

– понимание поведения в рантайме требует знания контейнера.

Плюс есть чисто практические моменты:

– проект может долго собираться;

– тесты могут быть «тяжёлыми», если поднимают контекст целиком;

– локальный запуск может требовать много ресурсов.

Это всё решается практиками (нормальные тестовые слои, модульность, профили сборки), но поначалу ощущается как «почему так сложно».

3.5. Когда выбирать Java

Сценарий 1. Долгоживущие системы, большие команды, высокая нагрузка

Java особенно хороша, когда вы строите систему, которая будет:

– жить 5–10 лет,

– постоянно развиваться,

– обслуживаться разными командами,

– иметь строгие требования к стабильности.

В таких условиях «чуть больше церемоний» превращается в «чуть меньше хаоса».

Сценарий 2. Банки, энтерпрайз и сложные домены

Java традиционно сильна в доменах, где:

– много бизнес-правил,

– сложные интеграции,

– требования по безопасности,

– аудит, контроль, регламенты,

– устойчивость важнее скорости эксперимента.

Плюс в этих сферах часто уже есть:

– инфраструктура под JVM,

– готовые библиотеки,

– опытные команды,

– стандарты разработки.

Сценарий 3. Когда важна наблюдаемость и эксплуатация

Если вы понимаете, что продукт будет «болеть» и его нужно будет лечить:

– расследовать деградации latency,

– ловить утечки памяти,

– разбирать thread dumps,

– анализировать GC,

то Java даёт хорошую базу и зрелые инструменты.

3.6. Вывод по Java

Java – это не «самая быстрая в написании», но одна из лучших для:

– предсказуемого роста,

– высокой нагрузки,

– больших команд,

– систем, которые нельзя «положить на пару часов».

Если вам нужен язык, где архитектура держится на типах, а эксплуатация поддержана инструментами – Java обычно оправдывает свою репутацию.

Глава 4. Go – плюсы/минусы

Go (или Golang) часто выбирают люди, которые любят, когда всё просто, предсказуемо и быстро. Это язык, который создавали с идеей:

«давайте писать серверы и утилиты так, чтобы их легко было собирать, деплоить и поддерживать».

Go редко восхищает «красотой абстракций», но часто восхищает тем, что:

– сервис собирается в один бинарник,

– работает стабильно,

– жрёт мало,

– и не просит от вас философии на тему DI-контейнеров.

4.1. Что обычно значит «Go-бэкенд»

Типичный Go-сервис:

– Go 1.21+ (или близко к актуальному).

– HTTP API на стандартной библиотеке `net/http` или на роутерах/фреймворках (их много, но часто берут минимум).

– Работа с БД через драйвер + тонкий слой репозитория.

– JSON, gRPC, очереди – всё по ситуации.

– Лёгкий контейнер/деплой: один бинарник + конфиг.

Go-сервисы часто выглядят «прямолинейно». И это не недостаток: это стиль.

4.2. Что поставить для работы

Минимально:

– Go (официальный дистрибутив).

– Редактор:

– VS Code + Go-плагин,

– или GoLand (если хотите «как в Java мире, но для Go»).

– Docker (БД, Redis, брокеры).

– Утилиты:

– `golangci-lint` (сборный линтер),

– `go test` (встроенные тесты),

– `pprof` (профилирование),

– `delve` (отладчик).

Go хорош тем, что «поставил Go – и почти всё уже есть». Это очень приятное чувство после миров, где нужно собрать пазл из 12 инструментов.

4.3. Плюсы Go

Плюс 1. Простота языка и предсказуемость

Go специально ограничивали по фичам, чтобы:

– код читался одинаково в разных командах,

– стиль был более-менее единым,

– поведение было понятным без «магии».

Поначалу кажется: «а где мои любимые фишки?»

Потом начинаешь ценить: «о, тут сложно написать слишком хитро».

Практический эффект:

– проще ревьюить код,

– проще онбордить людей,

– меньше «архитектурных религий» внутри команды.

Go как будто говорит: «давайте решим задачу, а не устроим конкурс абстракций».

Плюс 2. Быстрые бинарники и низкое потребление ресурсов

Go компилируется в один бинарник. Это даёт:

– простой деплой (особенно в контейнерах),

– меньше проблем с окружением,

– быстрый старт сервиса,

– хорошую эффективность по памяти и CPU (обычно).

Для платформенных команд это почти музыка:

– один файл,

– понятная конфигурация,

– легко катать в Kubernetes,

– быстро поднимать новые инстансы.

Если у вас много микросервисов, это ощущается особенно сильно: меньше инфраструктурной «возни».

Плюс 3. Отличная конкурентность: goroutines и каналы

Go создавался с мыслью, что сетевой сервер – это история про параллельность.

goroutines – очень лёгкие «зелёные потоки», которые Go runtime планирует сам.

В итоге вы можете:

– держать много одновременных соединений,

– параллелить I/O операции,

– строить пайплайны обработки.

Это не означает, что багов конкурентности не будет. Будут, конечно. Но модель проще, чем ручная работа с потоками во многих языках.

Часто Go выбирают именно за эту «естественность» конкурентного кода: сделал `go func()` – и задача поехала параллельно.

Плюс 4. Идеален для сетевых сервисов, инфраструктуры, high-load API

Go отлично показывает себя там, где нужно:

– много сетевых запросов,

– простая и быстрая обработка,

– низкие задержки,

– предсказуемость,

– эффективность на сервере.

Поэтому Go часто встречается:

– в API-шлюзах,

– в сервисах авторизации,

– в обработке событий,

– в инфраструктурных компонентах,

– в высоконагруженных микросервисах.

4.4. Минусы Go

Минус 1. Меньше «встроенной выразительности» для доменной логики (чем в Java)

Если доменная область сложная: много правил, много сущностей, много вариаций поведения – Java с её объектной моделью и типами иногда выражает такие вещи удобнее.

Go чаще тянет к стилю:

– простые структуры,

– явные функции,

– минимум «магии».

Это хорошо для инфраструктуры, но в сложной доменной логике вы можете столкнуться с тем, что:

– код начинает расползаться по функциям,

– появляются самодельные паттерны,

– хочется более богатой типовой системы «из коробки».

Это не «Go плох», это просто другой стиль. Но этот стиль подходит не всем доменам.

Минус 2. ORM/генерации/валидации – нужно выбирать аккуратно

В Go есть инструменты для:

– ORM,

– миграций,

– валидации,

– генерации клиентов/серверов.

Но важный нюанс: в Go-экосистеме есть много подходов, и не все одинаково зрелые и совместимые друг с другом. Можно легко собрать «зоопарк».

Что часто происходит:

– один пакет для роутинга,

– другой – для валидации,

– третий – для генерации,

– четвёртый – для БД,

– и всё это по-разному считает ошибки, контексты и структуру проекта.

Это лечится выбором «консервативного» набора инструментов и стандартов. Но новичку можно запутаться.

Минус 3. Дженерики есть, но экосистема ещё догоняет удобства Java/TS

В Go появились дженерики, и это полезно. Но:

– многие библиотеки и команды ещё адаптируются,

– часть паттернов в Go всё равно остаётся «ручной работой»,

– типовая система Go не пытается стать Java или TypeScript – она остаётся проще.

Если вы привыкли к богатому миру типовых абстракций, Go может казаться «немного деревянным». Иногда это плюс. Иногда – раздражает.

4.5. Когда выбирать Go

Сценарий 1. Высокая нагрузка, микросервисы, platform/infrastructure

Go – отличный выбор, если вы строите:

– много небольших сервисов,

– инфраструктурные компоненты,

– прокси, gateway, обработчики событий,

– high-load API с понятной логикой.

Там, где нужно «работает быстро и стабильно», Go часто оказывается в верхней части списка.

Сценарий 2. Когда важны простые деплои и эффективность

Если вам важно:

– быстро собирать и доставлять сервисы,

– легко масштабироваться,

– иметь минимальные зависимости,

– экономить ресурсы,

то Go очень часто выигрывает.

Сценарий 3. Когда команда ценит простоту и единый стиль

Если у вас команда, которая хочет:

– понятный код,

– минимум магии,

– предсказуемую сборку,

Go хорошо поддерживает этот стиль на уровне языка.

4.6. Вывод по Go

Go – это выбор, когда вам нужно:

– быстро,

– просто,

– эффективно,

– легко деплоить,

– хорошо работать под конкуренцией и нагрузкой.

Он может быть менее удобен для «богатой» доменной модели, зато очень хорош как рабочая лошадь для сетевых сервисов и инфраструктуры.

Глава 5. Сводные таблицы сравнения

Ниже – таблицы, которые помогают быстро «примерить» язык под задачу.

Важно: это не абсолютная истина, а практическая оценка типичного стека:

– Python: FastAPI + стандартные практики типизации/линтинга

– Java: Spring Boot

– Go: стандартный `net/http` + распространённые библиотеки

Оценки условные: низко / средне / высоко, иногда с пояснением.

5.1. DX (скорость разработки)

Комментарий:

– Python быстрее всего даёт результат «вчера».

– Java даёт результат «надёжно и по стандарту», но нужно разогнаться.

– Go часто даёт быстрый старт, но прототипирование бизнес-логики может быть медленнее, чем в Python.

5.2. Производительность и latency

Комментарий:

– Python может быть быстрым в I/O, но плохо переносит CPU‑тяжёлое в запросе.

– Java и Go – сильный выбор под нагрузку, но у Java может быть «дороже старт» и больше настройка, а Go обычно проще и легче.

5.3. Типизация и рефакторинг

Комментарий:

– В Python типизация работает, когда команда её реально использует.

– В Java архитектура часто держится на типах автоматически.

– Go строгий, но типовая система проще: меньше «выразительных конструкций», зато меньше сюрпризов.

5.4. Экосистема библиотек

Комментарий:

– Python выигрывает в data/ML и автоматизации.

– Java выигрывает в энтерпрайзе и интеграциях «как в банке».

– Go выигрывает в инфраструктуре и сетевых утилитах.

5.5. Наблюдаемость, диагностика, профилирование

Комментарий:

Java особенно сильна в эксплуатации больших систем: много стандартных инструментов, привычных практик. Go тоже очень неплох благодаря pprof и предсказуемому рантайму. Python требует более аккуратной инженерии (особенно при async).

5.6. Найм и доступность инженеров

Комментарий:

Python знают многие, но «Python для продакшена под нагрузкой» – уже не у всех. Java-инженеров много и часто с опытом больших систем. Go-инженеров меньше, но они часто приходят из high-load или инфраструктуры.

5.7. Риски и типовые «подводные камни»

Итог: как пользоваться этими главами

Если вы выбираете язык под сервис, задайте себе несколько честных вопросов:

1) Сколько лет будет жить система?

2) Насколько сложная доменная логика?

3) Какая нагрузка и какие требования к latency?

4) Насколько важны быстрые итерации и прототипы?

5) Какая у вас команда сейчас и кого реально нанять через 3–6 месяцев?

Дальше выбор обычно становится понятнее.

А если всё равно сложно – это нормально: иногда правильный ответ звучит так:

«Мы берём язык, который команда умеет эксплуатировать без героизма».

Итог: как пользоваться этими главами

Если вы выбираете язык под сервис, задайте себе несколько честных вопросов:

1) Сколько лет будет жить система?

2) Насколько сложная доменная логика?

3) Какая нагрузка и какие требования к latency?

4) Насколько важны быстрые итерации и прототипы?

5) Какая у вас команда сейчас и кого реально нанять через 3–6 месяцев?

Дальше выбор обычно становится понятнее.

А если всё равно сложно – это нормально: иногда правильный ответ звучит так:

«Мы берём язык, который команда умеет эксплуатировать без героизма».

Раздел II. Продукт и требования, единые для всех реализаций.

Ниже три главы одного раздела. Я буду писать так, чтобы это можно было взять как «техническое ТЗ для людей», а не как абстрактную теорию. Мы сначала договоримся что строим, затем – какие системные требования важны, и только потом – зафиксируем API-контракт до кода.

Глава 6. Домен TaskFlow: что строим

TaskFlow – это сервис управления задачами. Если очень коротко: “таски, проекты, комментарии, поиск, роли”. Если чуть длиннее – это маленькая копия того, что люди используют каждый день: Trello/Jira/Asana, но в более компактном виде и с понятным доменом, удобным для обучения.

Мы будем мыслить продуктом: не «сделать таблицы», а «помочь людям работать».

6.1. Главная идея продукта

У пользователя есть рабочие пространства (Workspace). Внутри них – проекты (Project). В проектах – задачи (Task), у задач – комментарии (Comment) и метки (Label).

Плюс обязательные вещи, без которых любой сервис задач быстро превращается в грустный список:

– регистрация и логин,

– права доступа,

– поиск и фильтрация,

– понятные ошибки в API,

– нормальная пагинация,

– аудит действий (кто что сделал).

Опционально (но мы предусмотрим место в дизайне):

– прикрепления файлов,

– уведомления (email/webhook).

> Важно: мы строим учебный, но реалистичный продукт. Поэтому всё, что “потом разберёмся”, мы хотя бы обозначим контрактами и моделями.

6.2. Сущности и их смысл

Ниже – не “таблицы базы данных”, а язык предметной области. Это сильно упрощает жизнь, когда вы будете писать код на любом языке.

6.2.1. User

User – это человек, который входит в систему.

Минимальные поля (логические):

– id – уникальный идентификатор

– email – логин (уникальный)

– password_hash – пароль в виде хеша (никогда не хранить “как есть”)

– name – отображаемое имя

– created_at, updated_at

Что важно сразу:

– email – уникальный.

– пароль не возвращаем через API никогда.

– имя может быть пустым или заполняться позже.

Вопрос продукта: может ли быть один пользователь в нескольких workspace?

Да, иначе сервис слишком ограничен. Это типичная модель “команд”.

6.2.2. Workspace

Workspace – рабочее пространство (команда/организация/группа).

Поля:

– id

– name

– owner_user_id (логический владелец)

– created_at, updated_at

Связи:

– Workspace содержит проекты.

– Workspace содержит участников (membership).

Почему Workspace нужен:

– отделяем проекты разных команд,

– вводим роли и права,

– создаём границы для поиска/доступа.

6.2.3. Роли и права (owner/admin/member)

Роли в рамках workspace:

– owner – главный, может всё, включая удаление workspace и управление правами.

– admin – почти всё, но не «уничтожить мир» (например, не сменить owner).

– member – обычный участник, работает с задачами и проектами по правилам.

Мы должны ответить на вопросы:

– кто может создавать проекты?

– кто может удалять проекты?

– кто может приглашать/удалять участников?

Для учебного проекта фиксируем разумное:

– owner/admin: управление участниками и проектами

– member: CRUD задач и комментариев (в пределах workspace)

– удаление workspace – только owner

> Да, роли всегда вызывают споры. Это нормально. Суть не в идеале, а в ясных правилах.

6.2.4. Project

Project – контейнер задач внутри workspace.

Поля:

– id

– workspace_id

– name

– description (опционально)

– status (например: active/archived)

– created_at, updated_at

Зачем статус:

– “архивировать проект” проще, чем “удалить навсегда”.

– архив не должен мешать поиску по умолчанию (но должен быть доступен через фильтр).

6.2.5. Task

Task – основная единица работы.

Поля (ядро):

– id

– workspace_id (или через project, но удобно иметь прямую привязку)

– project_id

– title

– description (опционально)

– status (todo/in_progress/done/canceled – минимально)

– priority (low/medium/high – опционально)

– assignee_user_id (опционально)

– reporter_user_id (кто создал)

– due_date (опционально)

– created_at, updated_at

Связи:

– Task имеет много комментариев.

– Task имеет много меток (many-to-many).

Сразу оговорим поведение:

– задача всегда принадлежит проекту;

– пользователь должен иметь доступ к workspace проекта, чтобы видеть задачу;

– менять статус может member/admin/owner (если есть доступ).

6.2.6. Comment

Comment – обсуждение задачи.

Поля:

– id

– task_id

– author_user_id

– body

– created_at, updated_at

– опционально: edited_at

Смысл:

– комментарии – это история решений. Поэтому удаление комментариев лучше делать мягким (soft delete) или запрещать полностью. В учебном варианте можно разрешить удаление автору/админу, но в аудит всё равно писать.

6.2.7. Label

Label – метка (тег), используемый для фильтрации.

Поля:

– id

– workspace_id

– name

– color (опционально)

– created_at, updated_at

Почему label живёт в workspace, а не в проекте:

– часто метки общие для всех проектов (“bug”, “feature”, “urgent”).

– проще делать единообразные фильтры.

6.3. Ключевые фичи (и что именно подразумеваем)

6.3.1. Регистрация и логин

Минимальный сценарий:

– пользователь регистрируется по email+паролю

– получает токен (например, JWT или opaque token)

– использует токен для запросов

Важно:

– регистрацию надо защищать от спама и брутфорса (rate limiting – обсудим в главе 7)

– ошибки должны быть понятными, но не слишком разговорчивыми (не рассказывать злоумышленнику, существует ли email)

6.3.2. CRUD проектов и задач

CRUD = Create/Read/Update/Delete, но в реальном продукте:

– “Delete” часто заменяется на архивирование или soft delete

– “Update” – частичное (PATCH), чтобы не отправлять каждый раз всю модель

Для учебной системы мы оставим:

– проекты: создать/получить/обновить/архивировать

– задачи: создать/получить/обновить/удалить (или тоже архивировать – на ваш выбор, но важно быть последовательными)

6.3.3. Поиск, фильтры, сортировка

Это то, что отличает “список задач” от “рабочего инструмента”.

Примеры фильтров для задач:

– по статусу

– по исполнителю

– по метке

– по сроку (due_date)

– по проекту

– по тексту (title/description)

Сортировка:

– по created_at

– по updated_at

– по due_date

– по priority

Поиск:

– простейший: q как подстрока по title/description

– продвинутый: отдельный поисковый движок (не обязательно в первой версии)

Сразу договоримся: фильтры должны быть комбинируемыми, а не “или это, или то”. Это влияет на дизайн API.

6.3.4. Прикрепления файлов (опционально)

Мы можем не реализовать полноценное хранение, но должны понимать модель:

Вариант A (практичный): файлы хранятся в объектном хранилище (S3-подобном), а API хранит только метаданные.

Сущность Attachment (пока концептуально):

– id

– task_id

– uploader_user_id

– filename

– content_type

– size

– storage_key или url

– created_at

Почему это важно даже “опционально”:

– нужно заранее решить: файл лежит у нас или мы выдаём ссылку?

– нужно заранее решить: кто может скачать файл?

– нужно заранее решить: как удалять/истекать ссылки?

6.3.5. Уведомления (email/webhook)

Уведомления почти всегда начинаются с двух событий:

– “задача назначена на меня”

– “в задаче новый комментарий”

Каналы:

– email (человеку)

– webhook (внешней системе: Slack/Teams/что угодно)

Мы будем думать об уведомлениях как о событии:

– событие возникло (task_assigned/comment_added)

– его нужно доставить подписчикам

– доставка может быть не мгновенной

– доставка может повторяться при ошибках

Даже если мы не делаем брокер сообщений в первой версии, мы можем заложить модель и API “подписки на webhooks”.

6.4. Границы доступа и типовые сценарии

Чтобы не сделать “дырявую” систему, полезно проговорить простые правила:

1) Пользователь видит только те workspace, где он участник.

2) Пользователь видит только проекты внутри доступных workspace.

3) Задачи доступны только через доступ к workspace/проекту.

4) Метки – внутри workspace, не глобальные.

5) Комментарии видны только тем, кто видит задачу.

Если вы соблюдаете эти пять пунктов, вероятность “случайно отдать данные другой команды” резко падает.

6.5. Что можно установить для комфортной работы (независимо от языка)

Чтобы не страдать с окружением:

– Docker Desktop или альтернативы (для БД/Redis локально)

– PostgreSQL (обычно удобнее как основная БД)

– DBeaver (просмотр БД)

– HTTP-клиент: Postman / Insomnia / или curl (curl – как хлеб: простой, но питательный)

– Markdown редактор (чтобы вести заметки по контрактам)

6.6. Мини-итог главы

Мы зафиксировали домен и сущности, а главное – общий смысл продукта. Дальше нам нужно договориться, как система должна себя вести под нагрузкой, как мы защищаемся от повторов и ошибок, как ведём аудит.

Глава 7. Нефункциональные требования

Нефункциональные требования – это то, из-за чего продукт либо выглядит профессионально, либо выглядит как “оно работало у меня на ноутбуке”.

В TaskFlow мы добавим набор требований, которые встречаются почти везде:

– rate limiting,

– идемпотентность (как паттерн для POST и “платёжных” операций),

– audit log,

– пагинация (cursor vs offset),

– SLA и наблюдаемость (observability).

Это всё звучит серьёзно, но в реальности это просто набор договорённостей и инструментов.

7.1. Rate limiting (ограничение частоты запросов)

Зачем нужно

Если вы открываете регистрацию и логин в интернет, то к вам придут:

– боты,

– перебор паролей,

– странные сканеры,

– и пользователи, которые “обновляют страницу каждые 200 мс”, потому что им тревожно.

Rate limiting решает две задачи:

1) Защищает инфраструктуру (CPU/DB/сеть).

2) Защищает пользователей (брутфорс на логин/пароль).

Что именно лимитируем

Минимум для TaskFlow:

– /auth/register – лимит на IP и/или на email

– /auth/login – лимит на IP и на аккаунт (email)

– “тяжёлые” списки (поиск задач) – лимит на пользователя

Как выражаем лимит

Удобно мыслить в терминах:

– N запросов за T секунд (например, 10 запросов/мин)

– отдельные лимиты для разных endpoint’ов

Пример политики (условно):

– регистрация: 5/час на IP

– логин: 10/10 минут на IP + 5/10 минут на email

– поиск задач: 60/мин на пользователя

Что возвращаем клиенту

Если лимит превышен, стандартно:

– HTTP 429 Too Many Requests

– заголовок Retry-After (сколько ждать)

И важный момент: сообщение в ответе не должно быть токсичным.

“Too many requests, retry later” – нормально. “Вы слишком много хотите” – это уже лишнее.

7.2. Idempotency для POST (и “платёжных” операций как паттерн)

Идемпотентность – это способность повторить запрос и получить тот же результат, не создав дубликатов.

Почему это важно:

– сеть ненадёжна,

– клиент может не получить ответ и повторить запрос,

– прокси может повторить запрос,

– пользователь может нажать “создать” дважды (классика жанра).

Где нужна идемпотентность

В TaskFlow критично для:

– POST /tasks (создание задач)

– POST /projects

– любые “операции эффекта” (например, отправка webhook/уведомления, загрузка файла – по дизайну)

Идея из платежей:

– клиент отправляет Idempotency-Key

– сервер по этому ключу понимает: это повтор или новый запрос

– повтор возвращает прежний результат

Как это выглядит

Клиент отправляет заголовок:

– Idempotency-Key: <random-uuid>

Сервер:

1) проверяет, есть ли запись по ключу + пользователю + endpoint’у

2) если есть – возвращает сохранённый результат (например, 201 и тело созданной задачи)

3) если нет – выполняет операцию, сохраняет результат и возвращает

Важные детали

– Ключ должен быть уникальным на сторону клиента (обычно UUID).

– Хранить идемпотентные ключи надо с TTL (например, 24 часа).

– Нужно привязывать ключ к пользователю (или workspace), иначе один пользователь сможет “повторить” чужую операцию.

Что сохранять

Минимально:

– статус-код,

– response body,

– время создания,

– fingerprint запроса (опционально).

И ещё правило: если по тому же ключу пришёл другой запрос (другая payload), можно вернуть 409 Conflict. Это честно и защищает от странных багов клиента.

7.3. Audit log (журнал аудита)

Аудит – это ответ на вопрос: кто и что сделал.

Даже если вы не банк, аудит полезен:

– для разборов инцидентов,

– для поддержки пользователей,

– для безопасности (“кто удалил проект?”),

– для аналитики.

Что писать в аудит

События минимум:

– user зарегистрировался

– user вошёл (логин)

– создан/обновлён/архивирован проект

– создана/обновлена/удалена задача

– добавлен/изменён/удалён комментарий

– изменения ролей и участников workspace

Структура записи (концептуально)

AuditEvent:

– id

– workspace_id (если событие в workspace)

– actor_user_id (кто сделал)

– action (например: task.created, project.archived)

– entity_type и entity_id

– timestamp

– metadata (JSON: старые/новые значения, ip, user-agent и т.п.)

Где хранить

Для учебного проекта:

– отдельная таблица в основной БД – нормально.

В больших системах:

– иногда события идут в отдельное хранилище/лог-систему.

Тонкий момент: PII

Если у вас есть персональные данные, не пишите в аудит лишнее.

Например, пароль и токены – никогда.

Email – можно, но лучше как идентификатор, не как “лог всего”.

7.4. Pagination: cursor vs offset (сравнение и выбор)

Списки задач, комментариев, проектов – это обязательно пагинация.

Есть два основных подхода:

– Offset pagination: limit=20&offset=40

– Cursor pagination: limit=20&cursor=…

Offset pagination

Плюсы:

– простая для понимания,

– легко прыгать на “страницу 5”.

Минусы:

– на больших данных может быть медленнее (offset заставляет БД “пропускать” строки),

– нестабильность при изменениях: если между запросами добавили записи, страница “поплывёт”.

Cursor pagination

Плюсы:

– стабильнее при добавлениях/удалениях,

– обычно лучше по производительности на больших объёмах,

– идеально подходит для “бесконечной ленты”.

Минусы:

– сложнее для клиентов,

– “страница 5” как концепция исчезает (есть только “вперёд/назад”, если реализовано).

Что выбираем для TaskFlow

Реалистичный выбор:

– для задач и комментариев: cursor pagination

– для простых справочников (labels, members) можно offset, но лучше быть последовательными

Cursor обычно строится на:

– (created_at, id) как “ключ сортировки”

– курсор кодируется в base64 и передаётся как строка

Пример сортировки:

– “новые сверху”: сортируем по created_at DESC, id DESC

– курсор хранит последнюю запись текущей страницы

7.5. SLA и Observability (наблюдаемость)

Что такое SLA в нашем контексте

SLA обычно формализуют в процентах доступности (“99.9%”). Для учебного проекта важнее другая мысль:

> Мы должны уметь доказать, что сервис жив, и быстро понять, если он болен.

Минимальные SLO (цели) для API:

– p95 latency для основных endpoint’ов, например < 300–500 мс (условно)

– error rate < 1%

– доступность (uptime) по health-check

Что нужно из observability

Минимальный набор, который стоит закладывать:

– структурированные логи (JSON, с request_id)

– метрики (количество запросов, latency, коды ответов)

– трассировка (trace_id) – опционально, но полезно

Обязательные практики:

– request_id генерируется на входе и прокидывается дальше

– логируем: метод, путь, статус, время обработки, user_id (если есть), workspace_id (если есть)

Health checks

Обычно:

– /health – быстрый, без БД (жив ли процесс)

– /ready – проверка зависимостей (БД доступна, миграции применены)

Нюанс: readiness не должен “ддосить” базу. Проверки должны быть лёгкими.

7.6. Мини-итог главы

Мы договорились о вещах, которые делают API взрослым:

– ограничиваем частоту,

– защищаемся от повторов,

– пишем аудит,

– выбираем пагинацию,

– делаем наблюдаемость.

Теперь мы готовы к самой полезной инженерной привычке: сначала контракт, потом код.

Глава 8. API Contract (OpenAPI) – пишем до кода

Эта глава – про дисциплину, которая экономит недели: сначала описываем API как договор, а потом реализуем.

OpenAPI – это формат, который позволяет:

– задокументировать endpoints,

– описать схемы данных,

– зафиксировать ошибки,

– автоматически генерировать документацию и клиентов (если нужно).

Мы не будем вставлять огромный YAML на 30 страниц. Вместо этого зафиксируем структуру контракта, модели и правила, а также ключевые endpoints.

8.1. Основные принципы контракта

1) Стабильные модели ошибок (один формат на весь API).

2) Понятные коды ответа (не “всегда 200”).

3) Пагинация единым способом для всех списков.

4) Аутентификация единым способом.

5) Версионирование с первого дня.

И ещё правило, которое спасает нервы:

> Если вы не можете объяснить endpoint одной фразой – скорее всего, он делает слишком много.

8.2. Auth model (модель аутентификации)

Для простоты (и реальности) выбираем:

– Bearer token в заголовке Authorization: Bearer <token>

Где токен берётся:

– из POST /auth/login (и, возможно, POST /auth/register сразу возвращает токен)

В OpenAPI это описывается как security scheme типа HTTP bearer.

Минимальные endpoints auth

– POST /auth/register

– POST /auth/login

– POST /auth/logout (опционально; зависит от того, храним ли сессии на сервере)

– GET /me (получить профиль текущего пользователя)

8.3. Error model (единая модель ошибок)

Самая частая боль API – когда ошибки везде разные. Мы сделаем единый формат.

Предлагаемая модель:

json

{

"error": {

"code": "validation_error",

"message": "Invalid request",

"details": [

{ "field": "email", "message": "Invalid format" }

],

"request_id": "req_123"

}

}

Где:

– code – машинно-обрабатываемый код (snake_case)

– message – коротко для человека

– details – массив деталей (опционально)

– request_id – чтобы найти запрос в логах

Типовые коды ошибок

– validation_error → HTTP 400

– unauthorized → 401

– forbidden → 403

– not_found → 404

– conflict → 409

– rate_limited → 429

– internal_error → 500

Важно: для 500 мы не раскрываем внутренности. Логи – для нас, клиенту – “internal_error”.

8.4. Pagination model (модель пагинации)

Мы выбрали cursor pagination для основных списков.

Запрос

Параметры:

– limit (по умолчанию 20, максимум например 100)

– cursor (опционально)

Пример:

GET /tasks?limit=20&cursor=eyJjcmVhdGVkX2F0IjoiLi4uIiwiaWQiOiIuLi4ifQ==

Ответ

Единый формат списка:

json

{

"items": [ … ],

"page": {

"next_cursor": "....",

"has_more": true

}

}

Правила:

– если has_more=false, next_cursor может быть null

– курсор непрозрачный для клиента (он не обязан понимать содержимое)

Если для некоторых endpoint’ов нужен offset – лучше не смешивать. Но если уж смешали, делайте разные endpoint’ы или разные модели ответа, чтобы клиент не гадал.

8.5. Versioning (версионирование)

Варианты:

– через путь: /api/v1/…

– через заголовок: Accept: application/vnd.taskflow.v1+json

Для простоты и ясности берём версию в пути:

– /api/v1

Почему:

– проще дебажить,

– проще проксировать,

– проще объяснить.

Правило:

– ломающее изменение – новая версия (/v2)

– не ломающее – расширяем текущую версию (добавляем поля, новые endpoints)

8.6. Endpoints: фиксируем основной набор

Ниже – список endpoint’ов, который покрывает домен из главы 6. Формат: метод, путь, смысл, основные ответы.

8.6.1. Auth / User

POST /api/v1/auth/register

Создать пользователя.

Request:

– email

– password

– name (опционально)

Responses:

– 201 → создан пользователь (+ возможно токен)

– 400 validation_error

– 409 conflict (email занят)

– 429 rate_limited

POST /api/v1/auth/login

Логин.

Request:

– email

– password

Responses:

– 200 → токен

– 400 validation_error

– 401 unauthorized

– 429 rate_limited

GET /api/v1/me

Текущий пользователь.

Responses:

– 200 user profile

– 401 unauthorized

8.6.2. Workspaces и участники

POST /api/v1/workspaces

Создать workspace.

Headers:

– Idempotency-Key (рекомендуется)

Responses:

– 201 workspace

– 401 unauthorized

– 400 validation_error

GET /api/v1/workspaces

Список workspace, где пользователь состоит.

Responses:

– 200 list (можно без пагинации, если их мало, но лучше с limit/cursor)

GET /api/v1/workspaces/{workspaceId}

Получить workspace.

Responses:

– 200

– 403 forbidden (если нет доступа)

– 404 not_found (можно вернуть 404 вместо 403, чтобы не “палить” существование)

POST /api/v1/workspaces/{workspaceId}/members

Добавить участника (owner/admin).

Request:

– user_email или user_id

– role (admin/member)

Responses:

– 201

– 403

– 404

– 409 (уже участник)

PATCH /api/v1/workspaces/{workspaceId}/members/{userId}

Изменить роль.

Responses:

– 200

– 403

– 409 (например, нельзя понизить owner “в никуда”)

DELETE /api/v1/workspaces/{workspaceId}/members/{userId}

Удалить участника.

Responses:

– 204

– 403

8.6.3. Projects

POST /api/v1/workspaces/{workspaceId}/projects

Создать проект.

Headers:

– Idempotency-Key (рекомендуется)

Responses:

– 201

– 403

– 400

GET /api/v1/workspaces/{workspaceId}/projects

Список проектов (с фильтром status=active|archived).

Responses:

– 200 paginated list

GET /api/v1/projects/{projectId}

Получить проект.

Responses:

– 200

– 403/404

PATCH /api/v1/projects/{projectId}

Обновить проект (name/description/status).

Responses:

– 200

– 400

– 403

8.6.4. Tasks

POST /api/v1/projects/{projectId}/tasks

Создать задачу в проекте.

Headers:

– Idempotency-Key (рекомендуется)

Request:

– title (required)

– description (optional)

– assignee_user_id (optional)

– priority (optional)

– due_date (optional)

– labels (optional: массив label_id)

Responses:

– 201 task

– 400 validation_error

– 403 forbidden

– 409 conflict (если идемпотентность конфликтует)

GET /api/v1/tasks/{taskId}

Получить задачу.

Responses:

– 200

– 403/404

PATCH /api/v1/tasks/{taskId}

Обновить задачу (частично).

Responses:

– 200

– 400

– 403

DELETE /api/v1/tasks/{taskId}

Удалить (или архивировать) задачу.

Responses:

– 204

– 403

GET /api/v1/workspaces/{workspaceId}/tasks

Список задач в workspace с поиском/фильтрами.

Query params (примерный набор):

– q – поиск по тексту

– project_id

– status

– assignee_user_id

– label_id

– sort (например created_at, updated_at, due_date)

– order (asc|desc)

– limit, cursor

Responses:

– 200 paginated list

8.6.5. Comments

POST /api/v1/tasks/{taskId}/comments

Добавить комментарий.

Headers:

– Idempotency-Key (можно, но не обязательно; полезно)

Request:

– body

Responses:

– 201

– 400

– 403

GET /api/v1/tasks/{taskId}/comments

Список комментариев (pagination).

Responses:

– 200

DELETE /api/v1/comments/{commentId}

Удалить комментарий (если разрешено правилами).

Responses:

– 204

– 403

8.6.6. Labels

POST /api/v1/workspaces/{workspaceId}/labels

Создать метку.

Responses:

– 201

– 400

– 403

– 409 (если имя метки уникально в workspace)

GET /api/v1/workspaces/{workspaceId}/labels

Список меток.

Responses:

– 200 (можно без пагинации, но лучше с limit/cursor)

8.6.7. Webhooks / Notifications (минимальный контракт)

Если делаем webhooks:

– POST /api/v1/workspaces/{workspaceId}/webhooks – создать подписку

– GET /api/v1/workspaces/{workspaceId}/webhooks – список

– DELETE /api/v1/webhooks/{webhookId} – удалить

Модель webhook:

– url

– events (например task.created, comment.created)

– secret (для подписи)

– is_active

Для email-уведомлений в учебной версии часто достаточно “внутренней отправки” без внешнего API. Но события всё равно должны быть в аудит/логах.

8.7. Коды ответов и “мелкая гигиена API”

Несколько правил, которые повышают доверие к API:

– POST создание → 201 Created (+ тело созданного ресурса)

– DELETE успешный → 204 No Content

– PATCH успешный → 200 OK (+ обновлённый ресурс)

– GET список → 200 OK + { items, page }

И ещё:

– даты/время – в ISO 8601 (2026-01-01T12:34:56Z)

– идентификаторы – строки (часто удобнее и переносимее)

– не возвращайте поля, которые клиент не должен видеть (пароли, секреты)

8.8. Что можно установить, чтобы удобно работать с OpenAPI

– Swagger UI или любая UI-обёртка для просмотра спецификации

– Postman/Insomnia – импортировать спецификацию и тестировать запросы

– Редактор YAML/JSON с подсветкой схем (любой нормальный IDE/редактор справится)

8.9. Мини-итог главы

Мы превратили требования продукта в набор понятных договорённостей:

– endpoints,

– модель ошибок,

– модель пагинации,

– модель аутентификации,

– версия API.

Теперь можно переходить к реализации на любом языке, не споря “как лучше назвать поле” на каждом шаге – всё уже зафиксировано контрактом.

Раздел III. База данных (одна на всех)

Глава 9. PostgreSQL: схема данных

Эта глава – про то, как спроектировать схему в PostgreSQL так, чтобы:

– API работало быстро и предсказуемо,

– данные не превращались в кашу через месяц,

– изменения можно было вносить без паники.

Мы будем держаться принципа: половина теории, половина практики. То есть: сначала понятные правила, затем – как это выглядит в схеме и что с этим делать руками.

9.1. Почему именно PostgreSQL (и что от него ожидать)

PostgreSQL – это “швейцарский нож” для бэкенда:

– умеет транзакции и строгую целостность;

– богатый SQL (CTE, оконные функции, JSON, полнотекст);

– отличные индексы и оптимизатор;

– работает одинаково хорошо и на ноутбуке, и на сервере.

Если вы планируете разрабатывать и учиться – Postgres благодарный. Он не делает вид, что всё в порядке, когда данные уже сломаны. Он честный: либо можно, либо нельзя.

Что установить для работы (по желанию):

– PostgreSQL (локально или в Docker)

– любой GUI-клиент: DBeaver, DataGrip, pgAdmin (что удобнее)

– psql (терминальный клиент; полезен как минимум для “быстро проверить”)

– Docker + docker-compose (чтобы одинаково запускать окружение)

9.2. Модель данных TaskFlow: от домена к таблицам

Напомним домен:

– User

– Workspace + участники (membership)

– Project

– Task

– Comment

– Label + связь task-label (many-to-many)

– Audit log

– Idempotency keys (для безопасного повтора POST)

Сразу полезное правило: таблица – это не “модель в коде”, а “факт в мире”.

Например, “участник workspace” – это факт, значит это отдельная таблица membership.

9.3. Нормализация: что это вообще и зачем вам это сегодня

Нормализация – это не ритуал. Это способ уменьшить вероятность того, что:

– вы храните одно и то же в пяти местах,

– оно расходится,

– и потом вы неделями чините отчёты и поиск.

9.3.1. Практическое правило №1: “Один факт – одно место”

Если роль пользователя в workspace хранится и в users, и в workspace_members, и в каком-то JSON – однажды вы поймаете ситуацию: “в одном месте admin, в другом member”. И вы проиграете спор с реальностью.

Роль в рамках workspace – это факт membership. Значит:

– workspace_members.role – единственный источник.

9.3.2. Практическое правило №2: “Массивы и списки – осторожно”

Например, хочется хранить метки как массив строк прямо в tasks.labels.

Это удобно первые два дня. Потом вы захотите:

– переименовать метку,

– вывести список всех меток workspace,

– посчитать статистику по меткам,

– не допускать дубликаты,

– индексировать фильтр по метке.

И внезапно вы понимаете, что метка – сущность, и у неё должна быть таблица.

Вывод: метки – отдельная таблица labels, а связь – task_labels.

9.3.3. Практическое правило №3: “Денормализация допустима, но осознанно”

Мы можем хранить workspace_id в tasks, даже если его можно вычислить через project. Это “снятие лишнего JOIN” для производительности и удобства фильтров.

Но тогда нужен механизм, который гарантирует консистентность (например, constraint, триггер, или правило в коде + тесты). В учебной системе можно держать workspace_id в tasks и контролировать его в приложении, а в идеале – усилить constraint’ами.

9.4. Схема данных: таблицы и ключевые поля

Ниже – списками, без таблиц (как вы просили). Названия – в стиле snake_case.

9.4.1. users

– id (uuid или bigserial; для современных систем uuid очень удобен)

– email (unique, not null)

– password_hash (not null)

– name (nullable)

– created_at (not null, default now())

– updated_at (not null, default now())

– deleted_at (nullable) – если хотите soft delete для пользователей (часто не надо; чаще “deactivated”)

Индексы:

– уникальный индекс на email

Constraints:

– email не пустой (можно простое CHECK (email <> ''))

– лучше хранить email в lower-case или нормализовать на входе

9.4.2. workspaces

– id

– name (not null)

– owner_user_id (not null, FK → users.id)

– created_at, updated_at

– deleted_at (nullable, если soft delete workspace)

Индексы:

– индекс на owner_user_id

Constraints:

– FK на owner

– CHECK (name <> '')

9.4.3. workspace_members

Это центральная таблица для прав.

– workspace_id (FK → workspaces.id)

– user_id (FK → users.id)

– role (not null; например: owner/admin/member)

– created_at

Ключи:

– составной PK (workspace_id, user_id) – так проще запретить дубль участия.

Constraints:

– role ограничить через CHECK (role IN ('owner','admin','member'))

или через PostgreSQL ENUM (ENUM удобен, но миграции могут быть чуть менее приятными; CHECK проще менять).

Индексы:

– индекс на user_id (быстрый поиск “где состоит пользователь”)

Практический нюанс:

– owner можно хранить и в workspaces.owner_user_id, и как запись membership с ролью owner.

Это полезно: membership даёт единый механизм прав, а поле owner – быстрый доступ и “один владелец”.

Тогда важно синхронизировать (обычно в коде + ограничениями).

9.4.4. projects

– id

– workspace_id (FK → workspaces.id)

– name

– description (nullable)

– status (active/archived)

– created_at, updated_at

– deleted_at (если soft delete проектов)

Индексы:

– (workspace_id, status) – список проектов по workspace

– возможно (workspace_id, name) если хотите искать по имени

Constraints:

– CHECK (status IN ('active','archived'))

9.4.5. tasks

– id

– workspace_id (FK → workspaces.id) – можно держать для ускорения фильтров

– project_id (FK → projects.id)

– title (not null)

– description (nullable)

– status (todo/in_progress/done/canceled)

– priority (low/medium/high, nullable)

– assignee_user_id (nullable, FK → users.id)

– reporter_user_id (not null, FK → users.id)

– due_date (nullable, date or timestamptz – зависит от требований)

– created_at, updated_at

– deleted_at (nullable; soft delete задач часто полезен)

Индексы (важное):

– (project_id, created_at desc, id desc) – для списков задач в проекте

– (workspace_id, status, created_at desc) – для фильтра по статусу

– (workspace_id, assignee_user_id, status) – “мои задачи”

– индекс на deleted_at не нужен, но частичный индекс полезен (см. ниже)

Partial index (практика):

– индекс только по “живым” задачам:

например индекс по (project_id, created_at desc) WHERE deleted_at IS NULL.

Это часто сильно помогает, потому что приложение почти всегда работает с живыми задачами.

Constraints:

– CHECK на статус и priority

– CHECK (title <> '')

Нюанс консистентности:

– если tasks.workspace_id хранится отдельно от projects.workspace_id, вы должны гарантировать, что они совпадают.

В простом варианте – правило в коде (при создании задачи берем workspace_id из проекта).

В строгом варианте – триггер или сложный constraint (в Postgres это решаемо, но для учебной книги можно оставить на уровне кода + тестов).

9.4.6. comments

– id

– task_id (FK → tasks.id)

– author_user_id (FK → users.id)

– body (not null)

– created_at, updated_at

– deleted_at (nullable)

Индексы:

– (task_id, created_at, id) – список комментариев к задаче

Constraints:

– CHECK (body <> '')

9.4.7. labels

– id

– workspace_id (FK → workspaces.id)

– name (not null)

– color (nullable)

– created_at, updated_at

– deleted_at (nullable)

Индексы:

– уникальность имени метки в workspace: unique (workspace_id, lower(name))

(обычно “Bug” и “bug” – одна и та же метка для людей)

9.4.8. task_labels

many-to-many связь:

– task_id (FK → tasks.id)

– label_id (FK → labels.id)

– created_at

Ключи:

– составной PK (task_id, label_id)

Индексы:

– индекс на label_id (чтобы быстро находить задачи по метке)

9.4.9. audit_events

– id

– workspace_id (nullable, FK → workspaces.id)

– actor_user_id (nullable, FK → users.id) – иногда события системные

– action (not null) – строка типа task.created

– entity_type (nullable) – task, project, …

– entity_id (nullable)

– ip (nullable)

– user_agent (nullable)

– metadata (jsonb, nullable)

– created_at

Индексы:

– (workspace_id, created_at desc)

– (actor_user_id, created_at desc)

– возможно (entity_type, entity_id)

9.4.10. idempotency_keys

Чтобы POST можно было безопасно повторять:

– id (uuid)

– user_id (FK → users.id)

– key (not null) – значение Idempotency-Key

– method (not null) – POST

– path (not null) – например /api/v1/projects/{id}/tasks

– request_hash (nullable) – чтобы ловить конфликт payload’ов

– response_code (not null)

– response_body (jsonb или text, not null)

– created_at

– expires_at

Индексы:

– unique (user_id, key, method, path)

– индекс на expires_at (чтобы чистить)

9.5. Индексы: как думать, чтобы не сделать “индекс на всё”

Индекс – это ускорение чтения ценой:

– места на диске,

– замедления записи,

– более сложного планирования.

9.5.1. Практическое правило: “индексируем то, чем фильтруем и сортируем”

Если ваш endpoint “список задач” почти всегда делает:

– WHERE workspace_id = ?

– AND status = ?

– ORDER BY created_at desc

То индекс должен соответствовать этой форме. Обычно порядок полей в индексе:

1) равенства (workspace_id, status)

2) сортировка (created_at, id)

9.5.2. “id в конце” для стабильной сортировки

Если вы делаете cursor pagination, вам нужна уникальная сортировка.

То есть сортируем по (created_at, id), а не только по created_at.

9.5.3. Partial indexes для soft delete

Если в таблице много удалённых записей, но вы почти всегда выбираете живые:

– делайте индекс с условием WHERE deleted_at IS NULL.

Это дешевле и эффективнее, чем индексировать всё подряд.

9.6. Constraints и foreign keys: когда база “воспитывает” приложение

Constraints – это ваша страховка от “случайно сохранили мусор”.

Что стоит делать всегда:

– NOT NULL там, где без значения сущность теряет смысл

– CHECK для ограниченных наборов значений (status, role)

– UNIQUE там, где нельзя дублировать (email, membership, label name)

– FOREIGN KEY почти везде, где есть ссылки

9.6.1. Foreign key и поведение при удалении

Варианты:

– ON DELETE RESTRICT – не даём удалить родителя, пока есть дети (часто лучший вариант)

– ON DELETE CASCADE – удаляем детей автоматически (опасно, но иногда удобно)

– ON DELETE SET NULL – “ссылка исчезла, но запись живёт”

Для TaskFlow здравый баланс:

– workspace → проекты: RESTRICT (или soft delete)

– project → tasks: RESTRICT/soft delete

– task → comments: RESTRICT/soft delete

– users в assignee: SET NULL (если пользователь деактивирован, задача остаётся, но без исполнителя)

9.6.2. Почему каскад иногда опасен

CASCADE легко превращается в кнопку “удалить всё случайно”.

Кто-то удалил workspace – исчезли проекты, задачи, комментарии, аудит (если вы не думали), и у вас остались только воспоминания.

9.7. Soft delete vs hard delete: как выбрать без философских войн

Hard delete – физическое удаление строки (DELETE FROM …).

Soft delete – отметка “удалено” (например deleted_at), но строка остаётся.

9.7.1. Когда soft delete – хороший выбор

– для задач и комментариев: полезно восстановление, аудит, разбор конфликтов

– для проектов: “архив” и “удаление” часто путают, лучше мягко

– для labels: чтобы не ломать историю и фильтры

9.7.2. Когда hard delete проще и честнее

– временные таблицы типа idempotency_keys (там вообще TTL)

– логины/сессии/refresh tokens (если вы их храните)

– технические данные, которые не нужны для истории

9.7.3. Цена soft delete

– каждый SELECT должен помнить deleted_at IS NULL

– индексы и запросы усложняются

– нужен механизм “чистки навсегда” (например, удалять soft-deleted старше N дней)

Практика: сделайте “живые данные” дефолтом:

– все основные запросы выбирают только deleted_at IS NULL

– отдельные admin-ручки могут показывать удалённые

9.8. Миграции: почему без них нельзя (и чем отличаются инструменты)

Миграции – это способ изменять схему базы управляемо, а не “я на проде руками поправлю”.

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

9.8.1. Flyway

Подход:

– миграции – SQL-файлы с версиями (V1__init.sql, V2__add_tasks.sql)

Плюсы:

– очень простой ментально: SQL – значит SQL

– легко ревьюить

– хорошо подходит для PostgreSQL

Конец ознакомительного фрагмента.

Текст предоставлен ООО «Литрес».

Прочитайте эту книгу целиком, купив полную легальную версию на Литрес.

Безопасно оплатить книгу можно банковской картой Visa, MasterCard, Maestro, со счета мобильного телефона, с платежного терминала, в салоне МТС или Связной, через PayPal, WebMoney, Яндекс.Деньги, QIWI Кошелек, бонусными картами или другим удобным Вам способом.