Skip to main content

Дизайн REST API, который выживает: версионирование, auth, ошибки и то, чему никто не учит

Задеплоить endpoint может кто угодно. API, которые переживают пять лет консьюмеров, миграций и инцидентов в 3 часа ночи, спроектированы вокруг скучных частей: версионирование, с которым можно жить, auth, который отказывает в закрытое состояние, ошибки, которые парсит машина, и пагинация, которая не врёт.

Дизайн REST API, который выживает: версионирование, auth, ошибки и то, чему никто не учит

Каждый туториал учит одному и тому же REST API: пять роутов, happy path, JSON на входе, JSON на выходе, готово за вечер. Потом API встречается с реальностью — мобильный клиент, который три недели не может обновиться, партнёрская интеграция, написанная поверх опечатки, которую теперь нельзя исправить, security-ревью, баг пагинации, который никого не списал дважды, но всем всё показал дважды, — и вы обнаруживаете, что туториал покрыл лёгкие 20%. Про остальные 80% и написан этот пост.

Это решения, которые мне приходилось защищать (или о которых жалеть) за годы production-API, — в том порядке, в каком они обычно кусают. И не случайно это ровно те темы, которые прощупывает system design интервью, когда интервьюер говорит: «а теперь спроектируйте для этого API».

Как версионировать API, который нельзя ломать?

Версионируйте с первого дня, в URL, и относитесь к каждому опубликованному полю как к контракту, который будете соблюдать годами. В момент, когда один консьюмер, которого вы не контролируете, выкатывает код поверх вашего API, «потом почистим» превращается в фикцию: переименовать поле — breaking change, удалить поле — breaking change, поменять тип — это инцидент с вашей фамилией. /v1/ в пути негламурен и слегка неэлегантен — и это подход, у которого скучные режимы отказа, а это высший комплимент в дизайне API.

Прежде чем идти решение за решением, вот весь пост одной таблицей — туториальный ответ, ответ, который переживает пять лет консьюмеров, и то, во что реально обходится срезанный угол, когда приходит счёт:

РешениеТуториальный ответОтвет, который выживаетВо что обходится срезанный угол
ВерсионированиеДобавляйте и переименовывайте поля свободно/v1/ с первого дня; после публикации — только аддитивные измененияКаждое переименование — инцидент внутри чужого приложения
Auth по умолчаниюДобавьте auth на те endpoint’ы, которым он нуженDeny by default; публичные роуты помечены явноТот единственный endpoint, который кто-то забыл, — краулер найдёт его раньше вас
АвторизацияВалидный токен означает, что вызывающему можноПроверяйте владение при каждом чтении ресурсаДанные вашего клиента, отданные другому клиенту
ОшибкиHTTP-статус плюс сообщение прозойМашинный код, человеческое сообщение, correlation IDИнтеграторы гадают; их баг-репорт становится вашим звонком в 3 часа ночи
Пагинация?page=3Непрозрачный cursor со смыслом «продолжай после этой строки»Элементы дублируются и исчезают под записями; глубокие страницы сканируют и выбрасывают
Аудитория документацииЛюди, которые её пролистаютСамый буквальный консьюмер, который у вас когда-либо будетАгент галлюцинирует параметр, который вы оставили расплывчатым

Настоящая дисциплина не в схеме URL, а в привычке к аддитивным изменениям. Новая возможность? Новое опциональное поле, новый endpoint — и никогда не изменённый смысл существующего. Команды, усвоившие правило «опубликовано — значит навсегда», проектируют поля внимательнее до публикации, и в этом и есть реальная польза: дисциплина версионирования — по большей части forcing function, заставляющая подумать дважды.

Как выглядит auth, который «fails closed»?

Каждый endpoint требует аутентификации, если он явно и осознанно не помечен как публичный — паттерн deny-by-default middleware, где забытая конфигурация роута означает, что он закрыт, а не открыт. Повторяющаяся катастрофа индустрии в API — не сломанная криптография, а endpoint, который кто-то забыл защитить и который нашёл краулер. Fail-closed дизайн делает такую ошибку структурно невозможной: ленивый путь и безопасный путь — один и тот же путь.

Авторизация заслуживает той же паранойи уровнем глубже: аутентификация говорит, кто вы, авторизация — что ваше, и зазор между ними — классическая утечка. GET /orders/12345 с валидным токеном всё равно обязан проверить, что заказ 12345 принадлежит пользователю этого токена — баг отсутствующей проверки владения (IDOR на языке безопасности) остаётся самой частой серьёзной уязвимостью API на код-ревью, и сгенерированный код воспроизводит его с энтузиазмом, потому что happy-path-версия выглядит идентично.

Почему ответы об ошибках заслуживают времени на дизайн?

Потому что ваши ошибки — тоже API, тот самый, который в 3 часа ночи потребляет уставший интегратор, решая, чей это баг — его или ваш. Живучий ответ об ошибке несёт три вещи: машиночитаемый код (insufficient_funds, а не просто HTTP 400), человекочитаемое сообщение с именем проблемного поля и correlation ID, по которому ваши логи и его баг-репорт находят друг друга. Одного HTTP-статуса слишком мало: 400, означающий пять разных вещей, заставляет каждого консьюмера парсить вашу прозу.

Предсказуемость — фича. Сюрприз — издержка. API, которые называют «приятными в интеграции», редко бывают хитрыми — они делают одно и то же везде.

Консистентность важнее богатства. Один error envelope, идентичная форма на каждом endpoint, задокументировано один раз. API, которые описывают как «приятные в интеграции», редко делают что-то хитрое — они делают одно и то же предсказуемое везде, поэтому обработка ошибок у интегратора — одна функция, а не по одной на endpoint. Предсказуемость — фича; сюрприз — издержка, тот же урок, который durability преподаёт на уровне архитектуры.

Что не так с пагинацией по номерам страниц?

Offset-пагинация (?page=3) врёт при конкурентных записях: строки, вставленные или удалённые, пока консьюмер идёт по страницам, заставляют элементы появляться дважды или исчезать, а глубокие offset-ы наказывают базу полным scan-and-discard. Она выживает в туториалах, потому что тривиальна и выглядит правильно в демо со статичными данными. Cursor-пагинация — непрозрачный токен со смыслом «продолжай после этой строки» — остаётся корректной под записями и быстрой на глубине, ценой потери «перейти на страницу 7», которая большинству реальных консьюмеров никогда не была нужна.

Переносимое правило: пагинируйте по позиции в упорядочении, а не по счёту от начала, всякий раз, когда данные меняются под читателями. Это маленькое дизайн-решение с длинным хвостом: прикручивать cursor-ы к уже отгруженному offset-API — ровно та миграция опубликованного контракта, о которой предупреждала секция про версионирование.

Меняется ли что-то, когда консьюмер — AI-агент?

Принципы выживают; допуски ужесточаются. Агенты потребляют API через tool definitions, и всё перечисленное выше становится машинно-ориентированным: расплывчатое сообщение об ошибке, мимо которого человек пожал бы плечами, отправляет агента в retry-цикл, неконсистентный envelope ломает его парсинг, недодокументированный параметр — галлюцинируется. Проектировать API, которыми агенты могут пользоваться надёжно, — точные schema, исчерпывающие коды ошибок, честные описания — то же ремесло, что хороший REST-дизайн, только без скидки на небрежность. В этом тезис объяснения MCP: интерфейсы инструментов — это дизайн API, второй раунд, с менее снисходительным консьюмером.

Вот почему путь по фундаментальным курсам по-прежнему проходит через построение реальных REST-сервисов — Node.js и production-API ради самого ремесла, на этаже backend-архитектуры, где решается, что сервис, а что handler, с TypeScript как дисциплиной типизации, которая делает контракты принудительными, а не декларативными. У каждого из этих навыков теперь есть второй заказчик: агент, вызывающий ваш API через schema, — ровно такой же буквальный, как тайпчекер, и заметно более дорогой, когда сбит с толку.

Чек-лист выживания

Версия в URL с первого дня; после публикации — только аддитивные изменения. Deny-by-default auth; проверка владения на каждом чтении ресурса. Один error envelope везде: машинный код, человеческое сообщение, correlation ID. Cursor-ы, а не offset-ы, всюду, где данные движутся. Документируйте для самого буквального консьюмера, который у вас когда-либо будет, — потому что с этого года он у вас есть. Ничто из этого не гламурно. Как и мост, который не падает.

Поделиться
X LinkedIn
Следующий шаг

Закрепите эту тему на курсе

Структурированный путь от теории к production-коду — с проектами и код-ревью.

Oleksii Anzhiiak

Автор статьи

Oleksii Anzhiiak

Софтвер-архитектор, Senior .NET инженер и со-основатель

Алексей Анжияк — софтвер-архитектор, Senior .NET инженер и со-основатель ToyCRM.com и ProfectusLab. Имея более 15 лет опыта, он специализируется на распределённых системах, облачной инфраструктуре, высоконагруженной backend-разработке и платформах аутентификации. Занимается проектированием архитектуры, созданием безопасных систем авторизации и разработкой современных образовательных программ, которые помогают студентам получить реальные карьерные результаты.

LinkedIn

Рекомендуем посмотреть

Подобранные сторонние видео по теме. Открываются на YouTube.

~1:56:00
Продвинутый Andrej Karpathy

Создаём GPT с нуля

Редкий практический разбор внутреннего устройства GPT — от теории к коду. Для инженеров, а не пользователей.

~27:00
Средний 3Blue1Brown

Трансформеры — технология за LLM (Deep Learning, глава 5)

Фирменное визуальное объяснение архитектуры трансформера от 3Blue1Brown. Лучший 30-минутный праймер для инженеров — сначала интуиция, потом математика.

~1:00:00
Начинающий Andrej Karpathy

[Часовой разбор] Введение в Large Language Models

Часовой разбор Карпатого: как реально работают LLM — inference, обучение, fine-tuning и зарождающийся LLM-OS. Самая ясная единая ментальная модель для инженеров, входящих в тему.

Связаться с нами