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

В такой ситуации возникает понятная идея: подключить мобильный клиент напрямую к существующей базе или быстро добавить несколько методов в старую систему. Это действительно позволяет показать первые экраны, но дальше начинают появляться проблемы. Клиент зависит от структуры таблиц, разные разделы возвращают данные в разных форматах, права доступа проверяются непоследовательно, а любое изменение монолита может сломать уже опубликованное приложение.

В этой статье разберу более устойчивый вариант: отдельный API‑слой между мобильным приложением и существующей системой. Без полной переработки монолита и без попытки сразу перейти на микросервисы.

Исходная ситуация

Возьмём обобщённый пример. В организации есть монолитная система, которая хранит данные пользователей, документы, статусы заявок и справочники. Сотрудники работают с ней через внутренний веб‑интерфейс. Теперь часть функций нужно перенести в мобильное приложение:

  • авторизацию пользователя;

  • просмотр профиля;

  • получение списка документов;

  • создание заявки;

  • просмотр её текущего статуса;

  • получение уведомлений.

На первом этапе кажется, что задача сводится к созданию нескольких HTTP‑методов. Но мобильный клиент отличается от внутреннего интерфейса. Он работает через публичную сеть, может отправлять повторные запросы, использовать устаревшую версию приложения и терять соединение в любой момент. Кроме того, после публикации нельзя одновременно обновить все установленные клиенты.

Поэтому API становится не просто способом получить данные из базы. Он превращается в стабильную границу между мобильным продуктом и внутренней инфраструктурой.

Почему нельзя подключать приложение напрямую к базе

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

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

API‑слой разрывает эту зависимость. Внутренняя модель может меняться, пока внешний контракт остаётся прежним.

Базовая схема решения

Для начала достаточно трёх уровней:

  1. Мобильный клиент отображает данные и отправляет пользовательские команды.

  2. Интеграционный API проверяет доступ, валидирует запросы, преобразует данные и формирует стабильный ответ.

  3. Монолит остаётся владельцем основной бизнес‑логики и данных.

API при этом не должен превращаться во второй монолит. Его задача на первом этапе достаточно узкая: защитить внутреннюю систему от прямого внешнего доступа и адаптировать её возможности под сценарии мобильного клиента.

Например, внутренний профиль может собираться из пяти таблиц, а приложению нужен один объект:

{
  "id": "2f43bb81-7839-4e84-99cb-9dc6e9d53c1f",
  "fullName": "Иван Петров",
  "email": "user@example.org",
  "status": "active"
}

Клиенту не нужно знать, в каких таблицах находятся имя, адрес и статус. Он получает модель, соответствующую конкретному экрану и пользовательскому сценарию.

Сначала контракт, потом реализация

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

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

paths:
  /v1/requests/{requestId}:
    get:
      summary: Получить заявку
      parameters:
        - name: requestId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "200":
          description: Заявка найдена
        "401":
          description: Пользователь не авторизован
        "403":
          description: Нет доступа к заявке
        "404":
          description: Заявка не найдена

Здесь важны не только путь и формат ответа. Нужно заранее определить:

  • может ли пользователь видеть только собственные заявки;

  • какие статусы возвращаются клиенту;

  • что происходит с закрытой или удалённой заявкой;

  • какие поля обязательны;

  • может ли поле отсутствовать или принимать null;

  • в каком часовом поясе передаётся дата;

  • что считается ошибкой клиента, а что ошибкой сервера.

HTTP уже задаёт общую семантику методов и кодов состояния. Например, GET предназначен для получения представления ресурса, а класс ответов 4xx описывает ошибки на стороне запроса клиента. Эти правила определены в RFC 9110. Чем меньше API изобретает собственных значений поверх стандартной семантики, тем проще его поддерживать на клиентах.

Единый формат ошибок

Если один метод возвращает ошибку строкой, другой объектом, а третий всегда отвечает 200 OK с полем success: false, мобильному разработчику приходится отдельно обрабатывать каждый сценарий.

Лучше заранее определить единый формат. В качестве основы можно использовать Problem Details, актуальная спецификация которого описана в RFC 9457:

{
  "type": "https://api.example.org/problems/request-not-found",
  "title": "Заявка не найдена",
  "status": 404,
  "detail": "Запрошенная заявка не существует или недоступна пользователю",
  "instance": "/v1/requests/2f43bb81-7839-4e84-99cb-9dc6e9d53c1f",
  "traceId": "f98e0ac0d71b4e3d"
}

Пользовательский текст и техническая диагностика здесь должны быть разделены. Приложение может показать понятное сообщение, а команда поддержки найти запрос по traceId. При этом в ответ нельзя выводить SQL‑запрос, стек вызовов или внутренние имена компонентов.

Отдельно проверить авторизацию на уровне объекта

Проверить токен недостаточно. После успешной аутентификации сервер всё равно должен определить, имеет ли конкретный пользователь право получить конкретный объект.

Типовая ошибка выглядит просто:

GET /v1/requests/10041
Authorization: Bearer <token>

Если сервер выбирает заявку только по requestId, пользователь может попытаться подставить другой идентификатор. Поэтому запрос к данным должен учитывать и объект, и текущего пользователя, либо проверять право доступа отдельным правилом домена.

OWASP ставит нарушение авторизации на уровне объектов на первое место в перечне рисков API Security Top 10 2023. В рекомендациях отдельно указано, что проверка должна выполняться в каждой функции, которая получает доступ к данным по идентификатору объекта. Полный перечень опубликован в OWASP API Security Top 10.

Важно также ограничивать поля ответа. Право увидеть заявку не всегда означает право получить все связанные с ней служебные данные. Лучше явно формировать DTO для внешнего API, а не сериализовать внутреннюю модель целиком.

Не переносить всю логику в API‑слой

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

Это создаёт два источника бизнес‑логики. Через некоторое время внутренний веб‑интерфейс и мобильное приложение начинают давать разные результаты, потому что одно правило изменили только в одном месте.

Если монолит уже умеет корректно выполнять операцию, API должен вызывать эту возможность через доступный внутренний интерфейс. Если такого интерфейса нет, можно добавить внутри монолита узкую точку интеграции. Прямое изменение таблиц из внешнего API стоит рассматривать только после отдельного анализа транзакций, ограничений и побочных эффектов. Запись в одну таблицу может выглядеть простой, но исходная операция также может создавать историю, отправлять событие или обновлять связанные данные.

Что делать, если монолит не имеет API

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

Самый безопасный вариант — добавить в монолит внутренние методы для нужных операций, а внешний API использовать как адаптер. Тогда бизнес‑правила остаются рядом с основной системой.

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

Для команд на изменение данных лучше избегать записи в копию базы. Реплика решает задачу чтения, но не становится новым владельцем исходных данных. Команды можно передавать в монолит через внутренний HTTP‑интерфейс, очередь сообщений или специально выделенный интеграционный модуль.

Я бы разделял чтение и изменение ещё на этапе проектирования. Для каждого метода полезно зафиксировать:

  • откуда читаются данные;

  • кто является их владельцем;

  • насколько они могут быть устаревшими;

  • куда передаётся команда;

  • когда операция считается завершённой;

  • что получит клиент при частичной недоступности систем.

Версионирование без бесконечного копирования методов

Версия в URL, например /v1, не решает проблему совместимости автоматически. Она только создаёт пространство, в котором контракт должен оставаться стабильным.

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

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

На сервере я бы собирал статистику запросов по версии API и версии клиента. Тогда решение об отключении старого контракта принимается по фактическому использованию, а не по предположению, что все уже обновились.

Кэширование и актуальность данных

Кэширование часто добавляют как универсальный способ ускорить API, хотя разные данные имеют разную допустимую задержку. Справочник может обновляться раз в сутки, профиль пользователя — после конкретного действия, а статус операции должен отображаться почти сразу.

Поэтому для каждого типа данных нужно определить период актуальности и событие, после которого кэш следует сбросить. Иначе приложение будет быстро показывать неправильную информацию.

HTTP поддерживает условные запросы и валидаторы представления, включая ETag и Last-Modified; их семантика также описана в RFC 9110. Для редко меняющихся ресурсов клиент может отправить If-None-Match, а сервер вернуть 304 Not Modified без повторной передачи тела ответа.

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

Наблюдаемость нужно проектировать вместе с API

Без журналирования и метрик почти любая ошибка мобильного приложения превращается в сообщение «у меня ничего не работает». Серверу необходимо связать запрос клиента с обращениями к внутренним системам и итоговым ответом.

Минимальный набор, который я бы заложил сразу:

  • идентификатор запроса или трассировки;

  • метод, маршрут и код ответа;

  • длительность обработки;

  • результат обращения к зависимым системам;

  • версию API и клиента;

  • технический код ошибки;

  • метрики количества запросов и доли ошибок.

Персональные данные, токены и содержимое документов в журнал попадать не должны. Логирование должно помогать восстановить ход выполнения операции, а не создавать вторую незащищённую копию данных.

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

Как внедрять такой слой поэтапно

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

  1. Описал внешний контракт.

  2. Определил владельцев данных и бизнес‑правил.

  3. Добавил интеграцию с монолитом.

  4. Настроил авторизацию на уровне объектов.

  5. Ввёл единый формат ошибок.

  6. Добавил метрики, трассировку и ограничения.

  7. Проверил поведение при недоступности зависимостей.

  8. Только после этого подключил мобильный интерфейс.

Такой подход близок к постепенному вытеснению старой системы: новые возможности появляются рядом с ней, а не требуют одномоментного переписывания. Мартин Фаулер описывает этот принцип как Strangler Fig и отдельно отмечает снижение риска по сравнению с полным переключением на переписанную систему. Исходное описание подхода доступно в статье Original Strangler Fig Application.

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

Что получается в итоге

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

Хороший результат выглядит так:

  • мобильный клиент не знает структуру внутренней базы;

  • внешний контракт проектируется под пользовательские сценарии;

  • права проверяются для каждого объекта и действия;

  • ошибки имеют единый машиночитаемый формат;

  • бизнес‑логика не дублируется без необходимости;

  • актуальность данных определена заранее;

  • запрос можно проследить от клиента до внутренней системы;

  • новые функции подключаются поэтапно.

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