Модуль 15. Итоговое приложение, Go-мост и ревью
Цель модуля: собрать финальный мессенджер, заменить mock-эндпоинты на реальный Go-бэкенд и провести объективное ревью по формализованному чек-листу.
К концу модуля у вас:
- Полноценный мессенджер: список чатов, комната, отправка, автокомплит, реакции, офлайн.
- Один Route Handler переключён с mock на Go-бэкенд — без изменения фронтенда.
- Чек-лист импеданса Go ↔ Zod пройден (даты, null, enums, пустые массивы).
- Ревью по чек-листу: auto-критерии зелёные, manual — с пометкой ревьюера.
Перед началом: Модули 1-14 (весь курс). Инструменты: OpenAPI, Go. Время: ~8 ч.
🔥 Инцидент. Команда бэкенда на Go готова начать. Но контракта API нет — каждый пишет свою версию. Frontend ждёт backend, backend ждёт фронтенд. Нужен OpenAPI-контракт: один источник правды для TypeScript-типов (Zod), моков (MSW) и Go-хендлеров.
📖 Теория
15.1 Финальная сборка мессенджера
Структура приложения
app/
├── app/
│ ├── layout.tsx # ThemeProvider + OfflineIndicator
│ ├── page.tsx # Главная: список чатов
│ ├── chat/[id]/page.tsx # Комната чата
│ ├── ui-gallery/page.tsx # Галерея компонентов
│ ├── offline/page.tsx # Fallback
│ └── api/
│ ├── health/route.ts # Smoke
│ ├── messages/route.ts # Сообщения (mock → Go)
│ ├── mentions/route.ts # Автокомплит (mock → Go)
│ └── emojis/route.ts # Эмодзи (mock → Go)
├── shared/api/ # OpenAPI, Zod, MSW
├── entities/
│ ├── message/ # $messages, Message тип
│ └── user/ # $currentUser, User тип
├── features/
│ ├── modal/ # стор модалки
│ ├── toasts/ # стор тостов
│ ├── mention-autocomplete/ # стор автокомплита
│ ├── offline-queue/ # стор офлайн-очереди
│ └── chat-input/ # виджет ввода
└── widgets/
├── chat-list/ # список чатов (Sidebar)
├── chat-room/ # комната: MessageList + ChatInput
└── message-list/ # VirtualList сообщений
Сборка виджета ChatRoom
tsx// app/widgets/chat-room/ChatRoom.tsx 'use client' import { useUnit } from 'effector-react' import { Box } from '@myorg/ui-kit' import { MessageList } from '@/widgets/message-list/MessageList' import { ChatInput } from '@/features/chat-input/ChatInput' import { $messages, $status } from '@/entities/message/model' import { MessageSkeleton } from './MessageSkeleton' export function ChatRoom({ chatId }: { chatId: string }) { const [messages, status] = useUnit([$messages, $status]) return ( <Box direction="column" style={{ height: '100%' }}> <Box style={{ flex: 1, overflow: 'hidden' }}> {status === 'loading' ? ( <MessageSkeleton /> ) : ( <MessageList messages={messages} /> )} </Box> <ChatInput chatId={chatId} /> </Box> ) }
15.2 Go-мост: генерация хендлеров из OpenAPI
Установка oapi-codegen
bashgo install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
Генерация Go-кода из OpenAPI
bash# Из спеки, созданной в Модуле 7 oapi-codegen -package api -generate types,server app/shared/api/openapi.yaml > api.gen.go
Результат — Go-типы и интерфейс сервера, соответствующие OpenAPI-спеке:
go// api.gen.go (автогенерируется) package api // Соответствует Zod-схеме UserSuggestionSchema из Модуля 7 type UserSuggestion struct { Id string `json:"id"` Name string `json:"name"` Avatar *string `json:"avatar,omitempty"` } type MentionsResponse struct { Suggestions []UserSuggestion `json:"suggestions"` } // Interface — сервер должен реализовать type ServerInterface interface { GetMentions(w http.ResponseWriter, r *http.Request, params GetMentionsParams) }
Минимальный Go-хендлер
go// main.go package main import ( "encoding/json" "net/http" "strings" ) type mentionsHandler struct{} func (h *mentionsHandler) GetMentions(w http.ResponseWriter, r *http.Request, params GetMentionsParams) { q := strings.ToLower(*params.Q) users := []UserSuggestion{ {Id: "1", Name: "Alice Johnson"}, {Id: "2", Name: "Bob Smith"}, {Id: "3", Name: "Charlie Brown"}, } var filtered []UserSuggestion for _, u := range users { if strings.Contains(strings.ToLower(u.Name), q) { filtered = append(filtered, u) } } json.NewEncoder(w).Encode(MentionsResponse{Suggestions: filtered}) } func main() { http.ListenAndServe(":8080", Handler(&mentionsHandler{})) }
15.3 Чек-лист импеданса Go ↔ Zod
Это самый важный раздел модуля. Контракт между Go-бэкендом и TS-фронтендом имеет подводные камни. Студент обязан пройти каждый пункт.
1. Дата-формат: RFC3339 (Go) vs ISO (JS)
ts// Go: time.Time → JSON → RFC3339: "2026-07-22T15:30:00Z" // JS: new Date().toISOString() → ISO 8601: "2026-07-22T15:30:00.000Z" // Zod-схема (Модуль 7): const MessageSchema = z.object({ createdAt: z.string().datetime(), // принимает ISO 8601 }) // Проверка: Go RFC3339 без миллисекунд → Zod принимает? // .datetime() в Zod 4 принимает RFC3339 по умолчанию ✓ // НО: часовой пояс "Z" vs "+03:00" — обе формы валидны
2. null vs omitempty
go// Go: nil-указатель → null в JSON; omitempty → поле отсутствует type User struct { Avatar *string `json:"avatar,omitempty"` // null или отсутствует } // Zod: // .nullable() — принимает null // .optional() — принимает undefined (поле отсутствует) // .nullable().optional() — и null, и undefined const UserSchema = z.object({ avatar: z.string().nullable().optional(), // обе формы OK ✓ })
3. Перечисления: Go string enum vs TS union
go// Go: type Role string const ( RoleAdmin Role = "admin" RoleMember Role = "member" RoleGuest Role = "guest" ) // TS/Zod: const RoleSchema = z.enum(['admin', 'member', 'guest']) type Role = z.infer<typeof RoleSchema> // 'admin' | 'member' | 'guest'
4. Пустые массивы vs nil
go// Go: var users []User // nil → JSON: null users := []User{} // пустой slice → JSON: [] // Zod: .array() требует массив, null → ZodError // Решение: .array().nullish().default([]) или Go всегда возвращает []User{} const ResponseSchema = z.object({ users: z.array(UserSchema), // Go должен возвращать [], не null })
Практическое задание
- Запустите Go-сервер на
:8080. - В
app/next.config.jsдобавьте proxy:jsasync rewrites() { return process.env.API_SOURCE === 'go' ? [{ source: '/api/:path*', destination: 'http://localhost:8080/:path*' }] : [] } API_SOURCE=go npm run build:app && npm run start:app- Проверьте: автокомплит упоминаний работает через Go — без изменения фронтенда.
- Запустите контракт-тест (Модуль 13): он должен проходить против Go-сервера.
15.4 Ревью: формализованный чек-лист
Принцип
Критерии максимально объективны и привязаны к конкретному артефакту. Всё, что можно автоматизировать, выполняет CI-бот.
Чек-лист
| Критерий | Вес | Источник | Как проверить |
|---|---|---|---|
| Соблюдены правила FSD | 4 | auto | npm run lint — import-x/no-restricted-paths |
| Все API-ответы валидированы Zod | 3 | auto | grep safeParse|\.parse( в Route Handlers + контракт-тест М13 |
| Core Web Vitals: CLS < 0.1, LCP < 2.5s | 3 | auto | Lighthouse CI отчёт |
| Покрытие тестов UI Kit ≥ 70% | 2 | auto | npm run test:coverage |
| size-limit не нарушен | 2 | auto | npm run size в CI |
| axe violations = 0 | 2 | auto | Storybook addon-a11y + Playwright |
| UI Kit не содержит Effector | 3 | auto | ESLint no-restricted-imports |
| Читаемость кода, уместность абстракций | 3 | manual | Peer-review или Алиса/OpenCode |
Расчёт: процент соответствия = (выполненный вес × 100) / Σ весов = (Σ 22).
Что проверяет Алиса/OpenCode
Скармливаем чек-лист + артефакты (Lighthouse, coverage, size-limit, ESLint, axe):
- «LCP 3.1s — выше целевого значения 2.5s»
- «size-limit нарушен для Modal: 5.2 KB > 5 KB»
- «импорт widgets→features в
features/chat/...нарушен» - «контракт-тест на
/api/emojisне покрыт Zod-схемой» - «coverage Text.tsx — 45%, порог 70%»
Компромиссы (Trade-offs)
| Решение | Плюсы | Минусы |
|---|---|---|
| OpenAPI как источник правды | Один контракт для TS/Go/MSW | Нужно поддерживать спеку |
oapi-codegen (Go) | Типы и интерфейсы из спеки | Конфигурация генерации |
| Reverse proxy (Next rewrites) | Фронт не меняется при mock→Go | Дополнительный хоп в запросе |
nullish().default([]) в Zod | Совместимость с Go nil slice | Неявный дефолт в схеме |
| Ручное ревью чек-листа | Ловит то, что CI не видит | Субъективно, долго |
15.5 Финальная задача
Добавить новую фичу (реакции) по всем слоям:
| Слой | Что сделать | Артефакт |
|---|---|---|
| UI Kit | Компонент Reaction с states | story + unit-тест |
| Контракт | OpenAPI-спек для POST /api/messages/:id/reactions | Zod-схема |
| Стор | $reactions, toggleReactionFx | unit-тест через effector/testing |
| Виджет | ReactionBar под MessageBubble | виджет + story |
| Route Handler | POST /api/messages/:id/reactions (mock) | MSW-хендлер |
| Go (опц.) | Сгенерировать хендлер из OpenAPI | api.gen.go diff |
15.6 Артефакты сдачи М15
| Артефакт | Критерий |
|---|---|
npm run verify | Полный зелёный |
Go-сервер запущен, /api/mentions отвечает | Autocomplete работает через Go |
| Контракт-тест против Go-сервера | Проходит без правок фронтенда |
| Lighthouse-отчёт (HTML) | LCP < 2.5s, CLS < 0.1 |
| size-limit вывод | Все компоненты в пределах |
| coverage-отчёт | Lines ≥ 70% |
| axe-отчёт | 0 violations |
| Ревью чек-лист | Все auto-критерии зелёные |
| Финальная задача: реакции | Все слои реализованы |
15.7 Чек-лист — конец курса
- Мессенджер работает: список чатов, комната, отправка, реакции.
- PWA: устанавливается, работает офлайн, офлайн-очередь (М12 — опц.).
- Один эндпоинт переключён на Go-бэкенд без изменения фронтенда.
- Чек-лист импеданса Go ↔ Zod пройден.
- LCP < 2.5s, CLS < 0.1 (Lighthouse на production-сборке).
- coverage ≥ 70%.
- size-limit не нарушен.
- axe violations = 0.
- Финальная задача (реакции) реализована по всем слоям.
-
@myorg/ui-kitготов к публикации (npm pack --dry-run чистый).
🎉 Курс завершён. Вы построили UI Kit как npm-библиотеку и мессенджер на React + Effector + Next.js с контрактом OpenAPI+Zod и мостом к Go-бэкенду. Артефакты курса применимы в реальных проектах.
🧩 Проверь себя
🔗 Мостик к выпуску
🎓 Вы прошли путь от пустого репозитория до публикуемого UI Kit и мессенджера с PWA, тестами и Go-мостом. Теперь вы — Frontend Architect.
Куда этот код переезжает дальше. UI Kit из курса — не учебный артефакт: в
команде он живёт внутри FSD-каркаса в слое shared, а фичи и виджеты
собираются над ним. Живой пример того же разделения слоёв на боевом стеке —
трек «Мессенджер на Go + React»: модуль
списка диалогов и
чата с realtime
повторяют архитектуру этого курса, только вместо mock-API — Go-бэкенд с
PostgreSQL и WebSocket.