API, TypeScript и runtime-контракты
Оглавление · HTTP · Повторы · Сравнение API
tRPC: один router, два способа вызова
appRouter в apps/web/src/server/router.ts объявляет четыре read-only процедуры.
| Процедура | Вход | Результат |
|---|---|---|
news.list | Нет | Новости, новые сначала |
news.bySlug | { slug: string } | Одна новость |
events.list | Нет | События по возрастанию времени начала |
events.bySlug | { slug: string } | Одно событие |
slugSchema требует строку длиной 1–120. Неизвестный slug даёт tRPC NOT_FOUND;
пустой или неверно типизированный вход отвергается валидатором.
Схема не запрещает пробелы или слэши — такие дополнительные требования пока не введены.
Серверный caller: src/server/api.ts вызывает appRouter.createCaller({}).
Caller доступен серверным потребителям без HTTP-запроса к собственному серверу.
Страницы используют CMS repository напрямую; router предоставляет тот же адаптер
через tRPC. Невалидный вход даёт BAD_REQUEST, отказ CMS — INTERNAL_SERVER_ERROR.
HTTP: route handler подключает Fetch adapter на /api/trpc и экспортирует GET/POST.
src/lib/trpc-client.ts создаёт браузерный клиент с httpBatchLink и относительным URL.
Сейчас этот helper подготовлен, но страницы не используют его для загрузки списка.
Пример для нового клиентского компонента:
import { trpc } from "@/lib/trpc-client";
const items = await trpc.news.list.query();
const item = await trpc.news.bySlug.query({ slug: items[0].slug });
Пример HTTP-чтения:
curl --fail http://localhost:3000/api/trpc/news.list
Запрос одной новости с URL-encoded JSON:
curl --fail --get --data-urlencode 'input={"slug":"news-152"}' \
http://localhost:3000/api/trpc/news.bySlug
Тип AppRouter экспортируется из серверного модуля и импортируется клиентом через
import type: он нужен при компиляции, а не как runtime-зависимость браузера.
Создание типизированного клиента само по себе не добавляет авторизацию, кэш запросов
или доступ к стороннему REST API.
Общий пакет contracts
packages/contracts/src/index.ts экспортирует:
slugSchema— вход процедуры bySlug.strapiWebhookSchema— вход webhook и задачи.StrapiWebhook— TypeScript-тип, выведенный черезz.infer.strapiEventName— строкуstrapi:content-changed.- NewsItem/EventItem/SiteContent/Page/FAQ/Partner и Zod schemas Blocks/media/дат из content.ts.
Zod выполняет проверку значений в работающем процессе. z.infer позволяет
не писать вручную второй интерфейс с теми же полями.
Пакет публикует из dist JavaScript и .d.ts; исходники сами по себе не являются
production-экспортом. После изменения contracts пересоберите пакет.
Контракт webhook
{
"event": "entry.publish",
"model": "news",
"entry": { "documentId": "article-1" }
}
| Поле | Требование |
|---|---|
event | entry.create, entry.update, entry.delete, entry.publish или entry.unpublish |
model | Непустая строка; allowlist моделей пока нет |
entry | Объект |
entry.documentId | Необязательная строка |
entry.id | Необязательная строка или число |
Другие поля entry | Сохраняются благодаря .passthrough() |
Оба идентификатора необязательны: { entry: {} } проходит схему. Инвалидирование
всего редакционного кеша не требует ID. Верхний объект не помечен passthrough; используйте результат parse,
а не исходный JSON, чтобы явно следовать договорённости о сохраняемых полях.
Для медиа используется отдельная ветка контракта:
{ "event": "media.update", "media": { "id": 42 } }
Поддерживаются media.create, media.update и media.delete. Поле media.id —
положительное целое число; model и entry для этой ветки не требуются.
Обе ветки инвалидируют кеш CMS и страниц. Webhook не публикует черновики.
Авторизованный trigger-test от кнопки проверки CMS возвращает
200 {"received":true} без изменения кеша.
Последовательность проверки webhook
- Наличие
STRAPI_WEBHOOK_SECRET. - Проверка заголовка
Authorization: Bearer <secret>черезtimingSafeEqual. - Чтение JSON и
safeParseобщей схемой. - Немедленное истечение тега
cmsи сброс кеша корневого layout. - Ответ
200 {"revalidated":true}; страницы обновляются при следующем посещении.
Невалидный payload возвращает 400, неверный секрет — 401, отсутствие настройки или ошибка сброса кеша — 503. Повторное уведомление безопасно повторяет инвалидирование. Собственной очереди доставки Strapi webhook нет. После истечения часа следующее обращение инициирует фоновое обновление, при этом читатель ещё может получить прежнюю страницу. При отказе CMS сохраняется последняя успешная версия, а следующая попытка зависит от новых обращений. Поэтому жёсткой верхней границы устаревания нет: без посещений или при длительном отказе источника оно может превышать час. Модель ISR Next.js.
Health endpoint
GET /api/health возвращает {"status":"ok"}. В нём нет запросов к PostgreSQL,
Strapi. Нельзя использовать его как доказательство готовности
всей системы. Для цепочки публикации проверяйте отдельно приём webhook и обновление страницы.
Как расширять API
Добавьте Zod-схему входа, новую процедуру и осмысленную проверку результата.
Рассчитывайте контекст и авторизацию до добавления операции изменения данных:
нынешний createContext: () => ({}) пуст и не содержит пользователя.
Не помещайте серверные секреты в переменные NEXT_PUBLIC_*, UI props или результат
процедуры. tRPC сохраняет удобство типов, но не исправляет утечку данных в коде.
Где смотреть код
Router, caller, HTTP adapter, webhook, contracts, браузерный клиент.
Готовность контента
GET /api/content-health читает опубликованный Site, news/events/pages/FAQ/partners через тот же repository. Успех — 200 с counts, отказ/невалидный CMS response — 503 с безопасным status unavailable. Данные записей и token не возвращаются. Пустые редакторские collections допустимы; отсутствие опубликованного Site — ошибка. Это дополняет /api/health, который проверяет только живой процесс.
GET /api/health возвращает { status: "ok" }; заголовок X-App-Release содержит SHA образа (локально local). PR CI использует его, чтобы отличить новую сборку от предыдущего работающего стенда.