Понять путь запроса через интерфейс, сервер и базу, понять границы модулей и выбрать хранение данных.
Чему научишься
•Объяснять путь запроса от интерфейса через сервер к базе данных.
•Различать операцию модуля и обращение к его внутренностям.
•Объяснять, зачем изменения структуры базы записывают миграциями.
Содержание · 13ОткрытьСкрыть
В этом материале разберём, какой путь проходит запрос от интерфейса до базы, как модули обращаются друг к другу и где хранить её данные. Если ты это уже знаешь, переходи к команде проверки и CI.
Зачем это в проекте
Карточка проекта «Каталог» появилась на экране сразу после нажатия кнопки. Сохранён ли проект? По одному экрану этого не понять, нужно знать, какой путь прошёл запрос и где остались данные. В платформе несколько частей, которые обращаются друг к другу. Заказчик просит, чтобы границы частей были описаны и код одной части не лез во внутренности другой.
Архитектура описывает основные части приложения, их обязанности и то, как они взаимодействуют. Стек называют набор технологий, которыми эти части реализованы, например TypeScript, серверный фреймворк и PostgreSQL. Фреймворк даёт готовую основу приложения. Например, Fastify в серверной части принимает запросы и вызывает код нужной операции.
Три части приложения
Frontend называют часть, с которой работает пользователь, обычно в браузере. В нашем примере он показывает форму и отправляет название проекта. Backend называют серверную часть. Он принимает запросы, проверяет их и работает с данными. База данных хранит данные между запусками приложения.
Браузер и сервер обмениваются запросами и ответами по HTTP (HyperText Transfer Protocol), протоколу, который задаёт их формат. В запросе есть метод, адрес операции и, когда нужно, данные. Создание проекта может выглядеть так:
POST /projects
{"name": "Каталог"}
201 Created
{"id": "p_catalog", "name": "Каталог"}
А открытие уже созданного проекта так:
GET /projects/p_catalog
200 OK
{"id": "p_catalog", "name": "Каталог", "repositories": ["catalog-api", "catalog-web"]}
Набор таких операций образует API платформы. API работает как договорённость о том, какие запросы можно отправить и какие ответы ожидать. Адреса и формат здесь приведены как пример, свои ты выберешь с агентом.
Путь одного запроса
Ответственный вводит название и нажимает кнопку. Frontend отправляет запрос серверу. Backend проверяет запрос, сохраняет проект в базе и возвращает результат. После этого интерфейс показывает созданный проект.
При повторном открытии интерфейс снова запрашивает данные у сервера, а сервер читает их из базы. Поэтому карточка сразу после нажатия ещё не подтверждает сохранение. Проект мог остаться только в памяти серверного процесса, которая очищается при его перезапуске. Открытая вкладка браузера при этом может показывать прежние данные. Чтобы проверить сохранность, после перезапуска нужно получить проект новым запросом к серверу и сравнить его идентификатор и название.
Backend проверяет входные данные, даже если frontend уже проверил форму. Код и данные, отправленные в браузер, доступны его пользователю, а запрос к API можно отправить и без интерфейса. Скрытое поле или отключённая кнопка серверную операцию не защищают.
Проверьте понимание
Агент сообщает, что проект «Каталог» создан и сохраняется. Что из перечисленного подтверждает, что проект переживёт перезапуск платформы?
1.
Ответ на GET /projects/p_catalog от того же процесса сервера, без перезапуска.
2.
Карточка «Каталог» на вкладке, которую не обновляли после перезапуска сервера.
3.
Успешный ответ на GET /projects/p_catalog после перезапуска с тем же идентификатором и названием проекта.
Модули с описанными границами
Backend платформы может быть одной программой. Внутри неё код всё равно делится по обязанностям: одна часть работает с аккаунтами, другая с проектами, третья обращается к GitHub. Вопрос в том, как эти части узнают что-то друг о друге.
Возьмём выдачу доступа. Перед тем как пригласить разработчика в catalog-api, код интеграции с GitHub должен узнать, добавлен ли разработчик в «Каталог». Проще всего прочитать это прямо из таблицы участников проекта:
SELECT person_id FROM project_members WHERE project_id = 'p_catalog';
Пока таблица не меняется, это работает. Потом в части, которая ведёт участие людей в проектах, меняют его устройство, например переименовывают таблицу. Тот, кто это меняет, проверяет свой код и его тесты. О запросе в коде интеграции он не знает. Хорошо, если запрос сразу упадёт с ошибкой. Хуже, когда изменение не вызывает ошибки, а меняет смысл данных, и запрос по-прежнему выполняется, но возвращает уже не то.
Модулем называют часть кода с одной обязанностью и описанными операциями. Операциями модуля пользуются другие части. Таблицы и внутренние функции модуля принадлежат ему одному. Если коду интеграции нужны участники проекта, он вызывает операцию модуля аккаунтов, например accounts.list_project_members("p_catalog"). Когда устройство участия меняется, автор изменения правит и эту операцию, а код интеграции продолжает получать верный список.
В брифе заказчик предлагает части платформы. В первой версии, MVP, нужны части, которые обеспечат доступ разработчика к проекту. К проекту подключаются инструменты команды, GitHub из них первый, поэтому модуль проектов знает, какие интеграции к проекту подключены. Описание границ трёх модулей может выглядеть так:
Модуль accounts (аккаунты)
Отвечает за: аккаунты сотрудников, роли и участие в проектах; решает, что сотруднику можно.
Операции: can(user, action, project_id), list_project_members(project_id),
add_member(user, project_id, person)
Хранит: свои таблицы. Другие модули их не читают и не меняют.
Модуль projects (проекты)
Отвечает за: проекты и подключённые к ним интеграции.
Операции: list_projects(user), get_project(user, project_id)
Пользуется: accounts.can.
Модуль github (интеграция с GitHub)
Отвечает за: связь GitHub-аккаунта, репозитории проекта, выдачу и отзыв доступа.
Операции: link_account(user, ...), grant_access(user, project_id, person), revoke_access(...)
Пользуется: projects.get_project, accounts.list_project_members.
Хранит: связанные GitHub-аккаунты и состояние выдачи доступа.
Тоже в MVP: модуль mcp (подключение агентов через MCP-сервер платформы) пользуется операциями projects и accounts
и ничего не хранит сам.
Имена модулей и операций здесь пример. Важно, что у каждого модуля названы обязанность и операции, а код соответствует описанию. Обращение к чужим таблицам или внутренним файлам в обход описанных операций нарушает границу, даже если сегодня оно работает.
Границу удобно держать двумя способами. Правило записывают в CODING_STANDARDS.md, и ревьюер сверяет с ним каждое изменение. Его же можно проверить автоматически. Для многих языков есть линтеры, которые запрещают импорт внутренних файлов чужого модуля, и такая проверка входит в команду проверки.
Проверьте понимание
В модуле github выдача доступа узнаёт участников «Каталога» прямым запросом к таблице модуля аккаунтов:
SELECT person_id FROM project_members WHERE project_id = 'p_catalog';
Позже модуль аккаунтов перестаёт удалять строку, когда человека исключают из проекта. Вместо этого он ставит в строке отметку времени removed_at. Модуль github никто не менял. Что произойдёт при следующей выдаче доступа?
1.
Модуль github получит ошибку базы при запросе участников проекта.
2.
Модуль github будет считать участниками и тех, кого исключили.
3.
Модуль github перестанет находить участников, которые остались в проекте.
Одна операция для API и MCP-сервера
Обработчик API принимает запрос и вызывает операцию модуля, например projects.get_project(user, "p_catalog"). Будущий MCP-сервер сможет вызвать ту же операцию. Тогда проверка прав и чтение проекта остаются общими для интерфейса и агента.
Модули или отдельные сервисы
Модули могут жить в одной программе. Такое устройство называют модульным монолитом. У него один запуск, а внутри код разделён на модули с описанными операциями. Модуль при этом означает часть кода, а не отдельный процесс.
Часть платформы можно вынести и в отдельный сервис. Так называют самостоятельную программу со своим запуском, к которой остальные обращаются по сети. Они опираются на её контракт, то есть описанные запросы и ответы. Например, модуль GitHub становится отдельным сервисом, а остальные модули вызывают его по HTTP.
Таблица в материале
Устройство
Что получаешь
Чем платишь
Модульный монолит
Один запуск. Вызов другого модуля работает как обычный вызов функции. Связанные записи разных модулей можно сохранить в базе вместе
Границу держат правила и проверки. Устройство запуска её не защищает
Отдельный сервис
У сервиса свой релиз и перезапуск. Свои секреты, например ключ интеграции с GitHub, есть только у его процесса
Второй запуск и его настройка. Контракт по сети. Платформа должна обрабатывать недоступность сервиса. Проверять приходится обе части вместе
Граница в обоих вариантах одна и та же. Отдельный сервис со своей базой не даёт обойти границу через код, но описывать контракт всё равно нужно. Поэтому заказчик требует описанных границ, а будут ли части отдельными сервисами, оставляет тебе. Предложение выделить сервис должно называть требование, которое этим решается, и цену для запуска и проверки.
Проверьте понимание
Ты думаешь вынести модуль GitHub в отдельный сервис. Какая причина оправдывает цену этого решения?
1.
Модуль GitHub нужно выпускать и перезапускать отдельно от остальной платформы.
2.
Модуль GitHub должен обращаться к другим модулям только через описанные операции.
3.
Модуль GitHub будет писать агент в отдельной сессии.
Как устроить код внутри модуля
Внутри модуля код тоже раскладывают по какому-то принципу. Чаще всего встречаются два.
По слоям. Код делят по роли: отдельно обработка запросов, отдельно бизнес-логика, отдельно работа с базой. Вариант с самыми строгими правилами называют clean architecture. Бизнес-логика в нём не знает о базе и фреймворке, а зависимости направлены к ней. Бизнес-логику при таком устройстве легко тестировать без базы. Зато одна новая возможность затрагивает файлы во всех слоях.
По фичам. Код делят по возможностям продукта: всё, что нужно для создания проекта, лежит рядом, от обработчика запроса до запроса к базе. Такой подход называют vertical slice architecture. Изменение одной возможности почти не выходит за одну папку, и агенту проще держать её в контексте. Зато общий код между фичами приходится выносить осознанно, иначе он повторяется.
Способы можно сочетать, например делить код на слои, а внутри слоёв раскладывать по фичам. Модули можно поставить снаружи. Тогда у каждого своя папка, а внутри неё слои или фичи. Можно и наоборот, модуль в каждом слое. Для этой платформы подходит любой из этих вариантов. Выбери тот, который понимаешь и сможешь объяснить агенту, и запиши выбор в решениях проекта. Тогда агент раскладывает новый код так же, как старый.
Мой совет
Код бэкенда я делю на четыре слоя, как в clean architecture: домен, то есть сущности платформы и их правила, бизнес-логику, API и инфраструктуру. Сценарии фич и обработчики запросов внутри слоёв раскладываю по фичам. Домен ни от чего не зависит, поэтому правила платформы проверяются тестами без базы. Сам домен я продумываю с агентом до кода. В grill-with-docs за это отвечает скилл domain-modeling.
Какие данные хранить и в какой базе
В MVP платформа хранит проекты и их репозитории, людей и их роли, участие людей в проектах, приглашения, связанные GitHub-аккаунты и состояние выдачи доступа. Для выбора базы важно, что записи связаны между собой. Участие связывает человека и проект, репозиторий принадлежит проекту, а связанный аккаунт принадлежит человеку.
Для связанных записей подходит реляционная модель с таблицами и связями. На старте удобно сравнить два варианта.
Таблица в материале
Вариант
Что получаем
Что учитываем
SQLite
База работает внутри приложения и хранит данные в файле; отдельный сервер базы не нужен
Файл должен переживать перезапуск и деплой. Одновременная запись ограничена
PostgreSQL
Отдельный сервер базы обслуживает подключения приложения и одновременную работу пользователей
Нужно организовать запуск базы, доступ к ней, хранение и резервное копирование
Мой совет
Я беру PostgreSQL сразу, потому что платформа командная и в ней появляются пользователи и права. Цена такого выбора в более сложном запуске, потому что сервер базы нужно поднимать вместе с приложением. Если тебе важнее быстрый старт, SQLite тоже подойдёт, а переход на серверную базу станет отдельной задачей позже.
Как база меняется вместе с приложением
Структура базы будет меняться от задачи к задаче. Например, когда разработчик связывает GitHub-аккаунт, платформе нужно хранить постоянный идентификатор этого аккаунта. Появляется новая таблица, а уже сохранённые люди и проекты не должны пропасть.
Поэтому каждое изменение структуры записывают в репозиторий как миграцию. Так называют отдельный шаг, который переводит существующую базу к новой версии приложения. Связанные аккаунты принадлежат модулю github, поэтому таблицу для них создаёт его миграция:
CREATE TABLE github_accounts (
person_id TEXT NOT NULL UNIQUE,
github_user_id BIGINT NOT NULL UNIQUE
);
Базу стоит создавать миграцией с самого начала. Тогда порядок изменений хранится в Git, и базу можно воспроизвести на другом компьютере и в CI. Таблицы модуля меняют миграции этого модуля, и граница сохраняется в базе. Инструмент для миграций агент подберёт под выбранный стек. Удалить все данные и пересоздать таблицы допустимо только для явно выбранной одноразовой тестовой базы.
Каркас до первой фичи
Если проверять выбранный стек сразу на фиче, ошибки настройки смешаются с ошибками самой фичи. Поэтому перед первой фичей собирают каркас. Так называют приложение, в котором уже есть все выбранные технологии: интерфейс, сервер и база, и один запрос проходит через них насквозь, но возможностей для пользователя ещё нет. Например, страница запрашивает у сервера состояние, а сервер читает из базы служебную запись, созданную миграцией:
GET /status
200 OK
{"server": "ok", "database": "ok", "schema_version": 1}
Ответ подтверждает, что запрос прошёл через интерфейс, сервер и базу и что схема создана. Но schema_version: 1 бывает и в новой пустой базе. Чтобы проверить сохранность, сравнивают конкретную служебную запись до перезапуска и при новом чтении после него.
Проверки каркаса запускают тесты платформы и миграции на чистой базе, а CI запускает те же проверки.
Решения, которые придётся принять
Стек. Нужны язык и серверный фреймворк, способ построить интерфейс, база с инструментом миграций и средства проверки. На выбор влияют три вопроса. На каком языке ты можешь читать код? Как платформа будет запускаться? Что понадобится дальше? Например, в MVP платформе нужен MCP-сервер, поэтому стоит заранее убедиться, что для выбранного языка есть библиотека MCP.
Мой совет
Фронтенд я пишу на TypeScript с Next.js на основе React: типы ловят часть ошибок до запуска, а маршрутизация и сборка в Next.js уже есть. Запросы к серверу веду через TanStack Query, который кэширует ответы сервера, а код фронтенда раскладываю по Feature-Sliced Design. Язык бэкенда бери тот, который знаешь, например Go, C#, Python, Java или TypeScript. Важнее, что ты читаешь код агента и замечаешь в нём ошибки. Фронтенд и бэкенд у меня общаются по HTTP, поэтому их можно писать на разных языках и запускать отдельно.
Архитектура. Нужно решить, будет ли платформа монолитом, модульным монолитом или набором сервисов, как модули будут обращаться друг к другу и как устроен код внутри модуля. Архитектуру выбирают в основе проекта, до первой фичи.
Мой совет
Решения я принимаю вместе с агентом: расспрашиваю его, прошу сравнить варианты и поискать свежие данные о технологиях, но последнее слово за мной. Агент часто говорит, что на старте хватит самой простой структуры. Для текущих требований это правда, но архитектуру дешевле заложить сейчас, чем переделывать, когда на ней уже построены десятки функций.
Как разделить на модули. Границы модулей выбирают не в основе проекта, а при проектировании первой версии продукта, когда понятно, какие возможности в неё входят. Части в брифе даны как пример. Можно держать аккаунты и проекты в одном модуле, а можно разделить их, как в примере выше. Чем больше модулей, тем больше операций нужно описать и поддерживать. Чем их меньше, тем больше кода меняется вместе. При любом разбиении у каждого модуля описаны обязанность и операции, и код их соблюдает.
Итог обсуждения с агентом может выглядеть так:
Сервер платформы работает одним приложением, модульным монолитом. Модули обращаются друг к другу только через описанные операции и не читают чужие таблицы. Код внутри модуля разложен по слоям, а внутри слоёв по фичам. Интерфейс обращается к API, а API и будущий MCP-сервер вызывают одни и те же операции. База PostgreSQL, схему создают миграции. Отдельные сервисы не выделяем, потому что независимый релиз частей пока не нужен.
Хорошее решение называет конкретные технологии и причину каждого выбора. Формулировки «популярно» или «агент хорошо знает» недостаточно. Из решения должно быть видно, как выбранный вариант запускается, проверяется и выполняет требования.
Углубление
Когда операция меняет несколько связанных записей, транзакция позволяет сохранить их целиком или отменить при сбое. Например, принятие приглашения создаёт членство в компании и закрывает приглашение, и обе записи должны измениться вместе.
Когда каркас будет готов, попроси агента проследить на твоём коде один запрос от страницы до базы и обратно и назвать файл на каждом шаге.
Попроси агента найти все места, где один модуль обращается к другому, и отметить те, что идут в обход описанных операций.
Документация выбранной базы о миграциях и резервных копиях.