Глава 45 / 51
Том 5 · Расширения, эксплуатация и практика

Практика 1: сервис REST API (бэкенд задач)

Одна глава сшивает первые четыре тома: REST-сервис с персистентностью, аутентификацией, пагинацией и горячей перезагрузкой — от пустого каталога до принятой приёмки.

Обзор

Позиционирование: первая из трёх практик и самая универсальная. Совет: делайте руками — в конце каждого раздела точка приёмки; все пройдены — проект готов. Ориентир: app.h/item.h из demo-single — зрелая форма ровно этого проекта.

45.1 Цель проекта

Собираем бэкенд «задач» (любой ресурс аналогично):

ВозможностьЭндпоинтыТома
CRUD ресурсаGET/POST /api/v1/todos, GET/PUT/PATCH/DELETE /api/v1/todos/{id}том 3 (14–18)
ПерсистентностьSQLite, без потерь при смене поколенийгл. 32 + гл. 28
Пагинация и фильтры?page=&size=&done=гл. 18
Токен-аутентификациязаголовок X-Api-Tokenгл. 27/39
Эксплуатационная поверхность/health, /stats, /admin/reloadгл. 26–27/38
Форма развёртыванияодин каталог + systemdгл. 42

45.2 Фундамент (30 минут)

  • Скелет: каталоги по конвенциям гл. 05 (modules/ route_http/ db/ wwwroot/); main.c оркестрирует include; xs.json — минимальная конфигурация по гл. 07/08
  • Три модуля: modules/http.h (семейство Reply из гл. 15), modules/db.h (DB_Init/Unit из гл. 32), modules/route.h (две таблицы гл. 17)
  • Таблица: todo(id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, done INTEGER DEFAULT 0, created INTEGER)
Точка приёмки 1: сервис стартует, все пять строк лога на месте (словарь гл. 02), /health отвечает ok.

45.3 CRUD и соглашения

Создание и обновление (два ключевых фрагмента)
/* POST /api/v1/todos: читаем JSON (гл. 16+18) → вставка с привязкой (гл. 32) */
static void Handle_Create(XS_HttpReq* pReq, const RouteParamHTTP* a, uint32 n)
{
    xvalue* pBody = ReadRequestJSON(pReq);        /* обёртка: body→parse */
    if ( pBody == NULL ) return (void)ApiError(pReq, 400, "bad_json");
    xvalue* pTitle = xrtValueObjectGet(pBody, "title");
    if ( pTitle == NULL || !IsNonEmptyString(pTitle) )
        return (void)(xrtValueRelease(pBody),
            ApiError(pReq, 422, "field_required:title"));
    ...
    int64 iId = DB_InsertTodo(ViewOf(pTitle));
    xrtValueRelease(pBody);
    ReplyItemJSON(pReq, 201, iId);            /* 201 + новый ресурс */
}

/* PUT/PATCH /api/v1/todos/{id}: параметры пути + частичное обновление */
/* трёхчастное соглашение об ошибках: 404 (нет) / 422 (поле) / 200 (успех) */

Соглашение об ошибках — заранее (ключевая привычка API-строителя): 400 битый JSON, 401 токен, 404 ресурс, 405 метод (маршрутизация сама), 413 превышение (драйвер сам), 422 семантика — у каждого машиночитаемое поле code. PUT отсутствующего ресурса: 404 или upsert? Выберите одно и впишите в документацию; двух не бывает.

Точка приёмки 2: семь CRUD-эндпоинтов проходят curl; трёхчастное соглашение ведёт себя верно на трёх нарочитых вводах.

45.4 Пагинация и аутентификация

  • Пагинация: список SELECT ... LIMIT ? OFFSET ? + тотал SELECT COUNT(*); ответ {"total":N,"page":P,"data":[...]} — верхний clamp (например size ≤ 100) обязателен как линия обороны
  • Аутентификационный слой: один модульный CheckToken(pReq) (сравнение за постоянное время), первой строкой каждого обработчика /api/*; токен в ручке Custom (строгое чтение гл. 09)
  • Учения перезагрузки: сменить правило сортировки списка → reload → проверить (а) запросы в полёте без ошибок (б) данные целы (поколения гл. 32) (в) счётчик непрерывен между поколениями (Swap несёт total — выпускной экзамен гл. 28)
Точка приёмки 3: под нагрузкой три смены без отказов; total непрерывен; неверный токен 401 и ограничение скорости работает.

45.5 Лист приёмки

ИзмерениеСтандартОснование
ФункцииCRUD/пагинация/фильтры/аутентификация — всё проходитэта глава
ПротоколHEAD поддержан (регистрация GET|HEAD), 405+Allow корректныгл. 15/17
Персистентностьбез потерь при рестарте и смене поколенийгл. 32
Устойчивостьсмена поколений под нагрузкой без отказов; четыре состояния лимитов верныгл. 28/09
Наблюдаемость/health /stats работают; логи запросов с IDгл. 43
Развёртываниеюнит systemd + окно корректной остановкигл. 42

Частые ошибки

Сперва эндпоинты, потом соглашение об ошибках
Симптом
Унификация формата ошибок в конце требует правки всех обработчиков.
Причина
Соглашение об ошибках — конституция API; фиксируйте первым шагом 45.3 (помощник ApiError + таблица code).
Параметр size без clamp
Симптом
Один ?size=1000000 — и сервис уходит в глубокие сканы пагинации.
Причина
Параметры пагинации — ввод клиента; верхний clamp и умолчание — часть интерфейса.

Упражнения

БазовыйЗавершить проект

Пройдите все точки приёмки 45.2–45.5.

Приёмка: все шесть измерений листа зелёные.
ПродвинутыйСемантика конкурентных правок

Добавьте поле updated и оптимистическую блокировку If-Match (409 при конфликте).

Приёмка: из двух конкурентных PUT успешен ровно один.
ВызовМаршрут документации OpenAPI

Реализуйте /api/openapi.json: опишите эндпоинты через xvalue (документация, ведомая таблицей маршрутов).

Приёмка: ноль расхождений документации с реализацией (генерация из одного источника).

Итоги

ТезисВывод
Потокскелет→три модуля→соглашение об ошибках→CRUD→пагинация и аутентификация→приёмка
Ключевые привычкисоглашение об ошибках первым; clamp параметров; единый вход аутентификации
Знак выпускасмена поколений под нагрузкой без отказов + данные целы + счётчики непрерывны
API, затронутые в главе
Что дальше по xrt