Модуль 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

bash
go 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
})

Практическое задание

  1. Запустите Go-сервер на :8080.
  2. В app/next.config.js добавьте proxy:
    js
    async rewrites() {
      return process.env.API_SOURCE === 'go'
        ? [{ source: '/api/:path*', destination: 'http://localhost:8080/:path*' }]
        : []
    }
  3. API_SOURCE=go npm run build:app && npm run start:app
  4. Проверьте: автокомплит упоминаний работает через Go — без изменения фронтенда.
  5. Запустите контракт-тест (Модуль 13): он должен проходить против Go-сервера.

15.4 Ревью: формализованный чек-лист

Принцип

Критерии максимально объективны и привязаны к конкретному артефакту. Всё, что можно автоматизировать, выполняет CI-бот.

Чек-лист

КритерийВесИсточникКак проверить
Соблюдены правила FSD4autonpm run lint — import-x/no-restricted-paths
Все API-ответы валидированы Zod3autogrep safeParse|\.parse( в Route Handlers + контракт-тест М13
Core Web Vitals: CLS < 0.1, LCP < 2.5s3autoLighthouse CI отчёт
Покрытие тестов UI Kit ≥ 70%2autonpm run test:coverage
size-limit не нарушен2autonpm run size в CI
axe violations = 02autoStorybook addon-a11y + Playwright
UI Kit не содержит Effector3autoESLint no-restricted-imports
Читаемость кода, уместность абстракций3manualPeer-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 с statesstory + unit-тест
КонтрактOpenAPI-спек для POST /api/messages/:id/reactionsZod-схема
Стор$reactions, toggleReactionFxunit-тест через effector/testing
ВиджетReactionBar под MessageBubbleвиджет + story
Route HandlerPOST /api/messages/:id/reactions (mock)MSW-хендлер
Go (опц.)Сгенерировать хендлер из OpenAPIapi.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-бэкенду. Артефакты курса применимы в реальных проектах.


🧩 Проверь себя

1. Итоговый ревью курса: что доказывает что архитектура работает?
2. Go BFF (Backend-for-Frontend) — отличие от API Gateway?
3. Как доказать что архитектура удалась?
4. Итог: главное правило связей слоёв (FSD + monorepo)?
5. Go-мост (reverse proxy / BFF) — зачем для frontend?

🔗 Мостик к выпуску

🎓 Вы прошли путь от пустого репозитория до публикуемого UI Kit и мессенджера с PWA, тестами и Go-мостом. Теперь вы — Frontend Architect.

Куда этот код переезжает дальше. UI Kit из курса — не учебный артефакт: в команде он живёт внутри FSD-каркаса в слое shared, а фичи и виджеты собираются над ним. Живой пример того же разделения слоёв на боевом стеке — трек «Мессенджер на Go + React»: модуль списка диалогов и чата с realtime повторяют архитектуру этого курса, только вместо mock-API — Go-бэкенд с PostgreSQL и WebSocket.