Architecture First: как небольшой командой с ИИ сделать альтернативу Jira — Часть 23.
В предыдущей статье мы разобрали комментарии. Интерфейс для них, как и для истории изменений, делал Николай. Эта статья — о том, как он переписал весь фронтенд.
Краткое содержание
Как мы получили новый фронтенд:
- Старый интерфейс стал неудобным: с каждой новой функцией он усложнялся, а бэкенд-разработчик не знал, как улучшить UI и UX.
- Весь фронтенд переписал Николай — фронтенд-разработчик, который присоединился к проекту летом.
- В новой архитектуре каждая страница явно объявляет, какие операции ей нужны, а реализации этих операций лежат отдельно и обращаются к API.
- Ожидаемые ошибки операции — часть её типа: страница знает, что запрос может вернуть «нет доступа» или «задача не найдена», и показывает это без исключений.
- Типы API генерируются из OpenAPI-описания бэкенда.
- Код проверяется тестами на Vitest, линтерами и проверкой типов перед каждым коммитом.
- Правила архитектуры записаны в
AGENTS.md: Николай много работает с ИИ, и этот файл задаёт правила для агентов, которые пишут код.
Почему интерфейс пришлось переписать
Первую версию веб-интерфейса мы делали сами, собирая страницы из прототипов, сгенерированных ИИ, — об этом была вторая часть цикла. Для старта этого хватило, но с каждой новой функцией — спейсами, организациями, правами, атрибутами — интерфейс становился сложнее и неудобнее.
Над Boards тогда работал один бэкенд-разработчик, и как улучшить UI и UX, он не знал. Новые экраны добавлялись, но общий интерфейс от этого лучше не становился.
Летом к проекту присоединился Николай, фронтенд-разработчик. Сначала он поправил несколько вещей в бэкенде, чтобы удобно запускать проект локально, — о них расскажем в следующей статье. А потом переписал весь фронтенд: в одном пулл-реквесте с новым интерфейсом изменилось почти пятьсот файлов.
Страница объявляет свои зависимости
Главное в новой архитектуре — у каждой страницы есть тип, который перечисляет операции, нужные ей для работы. Например, страница задачи:
export type IssuePageDeps = {
deleteIssue: DeleteIssue
issueDetails: IssueDetailsDeps
moveIssue: MoveIssue
updateIssue: UpdateIssue
viewIssuePage: ViewIssuePage
}
Каждая операция — отдельный тип в своём файле. Он описывает вход, результат и ошибки, которые можно ожидать:
type DeleteIssueResult = null
type DeleteIssueError =
| 'AccessDenied'
| 'IssueNotFound'
| 'TemporarilyUnavailable'
export type DeleteIssue = (input: {
issueKey: string
}) => Promise<ActionResult<DeleteIssueResult, DeleteIssueError>>
Операция не бросает исключений: она возвращает либо результат, либо одну из перечисленных ошибок. Список ожидаемых ошибок виден прямо в типе, поэтому страница знает, что задачу могли уже удалить или доступ к ней могли отозвать, и показывает это пользователю.
Сама страница не знает, как выполняется операция: через HTTP, из кэша или из тестовых данных.
Реализации живут отдельно
Реализации операций лежат в папке infrastructure, по одному файлу на операцию. Только там есть HTTP, сгенерированные типы API и обработка сетевых ошибок:
export const openApiDeleteIssue =
(baseUrl: string): DeleteIssue =>
async ({ issueKey }) => {
const client = createApiClient(baseUrl)
try {
const response = await client.DELETE('/api/issues/{key}', {
params: { path: { key: issueKey } },
})
switch (response.response.status) {
case 401:
case 403:
return err('AccessDenied')
case 404:
return err('IssueNotFound')
default:
break
}
if (!response.response.ok) {
return err('TemporarilyUnavailable')
}
return ok(null)
} catch {
return err('TemporarilyUnavailable')
}
}
Здесь HTTP-статусы превращаются в ошибки, которые понимает страница. Страница не видит ни 404, ни DTO из API, только IssueNotFound.
Типы для запросов к API не пишутся вручную. Незадолго до прихода Николая мы включили в бэкенде OpenAPI-описание, и фронтенд генерирует по нему типы командой pnpm api (через openapi-typescript), а запросы отправляет через openapi-fetch. Если в бэкенде поменяется контракт, фронтенд перестанет собираться.
Собираются зависимости в файле маршрута Nuxt. Он создаёт реализации и передаёт их странице:
const deps = {
deleteIssue: openApiDeleteIssue(baseUrl),
issueDetails: {
loadAssignees: openApiLoadAssignees(baseUrl),
loadMoveBoards: openApiLoadMoveBoards(baseUrl),
loadMoveSpaces: openApiLoadMoveSpaces(baseUrl),
loadStatuses: openApiLoadStatuses(baseUrl),
},
moveIssue: openApiMoveIssue(baseUrl),
updateIssue: openApiUpdateIssue(baseUrl),
viewIssuePage: openApiViewIssuePage(baseUrl),
}
Зависимости передаются явно, сверху вниз. Глобального хранилища и provide/inject для них нет: по коду страницы видно всё, что ей нужно.
Тесты на Vitest
Тесты написаны на Vitest. В первой версии они проверяли в основном функции, которые переводят ответы API в данные для страницы, и вспомогательную логику. Такие тесты работают без браузера и без бэкенда.
Раз страница зависит только от типов операций, её можно проверить, подставив вместо настоящих реализаций тестовые. Тесты компонентов в браузере с такими подставными зависимостями появились позже. Перед коммитом запускается одна команда, которая исправляет форматирование, проверяет типы, гоняет тесты и собирает проект:
"pre-commit": "pnpm fix && pnpm typecheck && pnpm test && pnpm build"
Правила для ИИ-агентов
Николай много работает с ИИ и перенёс свой подход к разработке во фронтенд Boards. Правила архитектуры записаны в файле AGENTS.md, который начинается так: «This file is the source of truth for changes in this repository». Его читают и люди, и ИИ-агенты, которые пишут код.
В файле описано всё, о чём говорилось выше: как зависимости передаются от маршрута к странице и её компонентам, где лежат контракты и реализации, что компоненты интерфейса не должны импортировать типы API. Есть и правила против лишних абстракций — например, не выносить общий контракт, пока у него нет второго реального потребителя, и не добавлять фабрики и обёртки там, где реализация может напрямую выполнить операцию страницы.
Такие правила особенно полезны, когда код пишет ИИ: агент склонен добавлять «на всякий случай» слои и общие сервисы, а явный запрет удерживает код простым.
Как мы делили работу
Задачи на фронтенд брались с той же доски в Boards, а изменения приходили пулл-реквестами. Бэкенд-часть изменений проходила ревью. А фронтенд мы доверили Николаю целиком.
Итоги
- Интерфейс переписали, когда он перестал быть удобным. С каждой функцией он усложнялся, а улучшать UX было некому.
- Страница явно объявляет зависимости. Каждая операция — отдельный тип с результатом и ожидаемыми ошибками.
- HTTP и типы API — только в инфраструктуре. Страница видит ошибки вроде
IssueNotFound, а не статусы HTTP. - Типы API генерируются из OpenAPI бэкенда. Расхождение контрактов ловится при сборке.
- Тесты, линтеры и типы — перед каждым коммитом.
- Правила — в
AGENTS.md. Они одинаковы для людей и для ИИ-агентов.
Что дальше
Прежде чем переписать фронтенд, Николай поменял несколько вещей в бэкенде: токен переехал из localStorage в HttpOnly-cookie, появились настройки CORS и docker-compose для локального запуска. Об этом — следующая статья.