Каких ошибок следует избегать, проектируя свое REST API
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.
Меня зовут Максим Ливак. Я Java-разработчик из команды ITOMYCH STUDIO. Нам приходилось работать со сложными проектами в финтех-сфере. Важнейший момент в этом направлении — легкость обмена данными между сервисами.
Вот почему я обожаю API: они позволяют обмениваться данными между проектами и продуктами. Рождаются комплексные приложения. Со стороны пользователя выглядят, как магия.
С API я работал довольно плотно, больше трёх лет разрабатывал Middleware API. Оно подразумевает интеграцию со многими внутренними API для получения необходимой информации о клиенте, вендорах банка или вызова нужных функций — создание платежа, вызов ОТП-проверки.
Со всеми этими API есть одна проблема: они не придерживаются одного стандарта. То есть каждый разработчик проектировал его как «подсказывало сердце». С подобными API сложно интегрироваться, ведь сразу не понятна задумка «автора». Один такой использовал буквы вместо имен полей (*в зале звучат нервные смешки*).
Что такое REST API
Прежде чем оценивать ошибки, определимся, что такое REST и какие правила следует соблюдать разработчику, проектируя такое API. По теории есть много статей, так что я сфокусируюсь на практической части.
Концепцию REST разработал в 2000 году Рой Филдинг (Roy Fielding), соавтор протокола HTTP. REST — шаблон проектирования, основанный на возможностях протокола HTTP. В этом вся прелесть: можно общаться между собой независимо от языка, фреймворка или платформы.
REST расшифровывается как REpresentational State Transfer, т. е. «передача репрезентативного состояния». По-человечески, это передача данных о текущем состоянии системы в довольно очевидной форме, которая «объясняет сама себя». С правильно спроектированным REST API легко работать даже при скудно оформленной документации.
Однако у REST есть свои преимущества и недостатки. Этот подход не в каждом случае полностью рабочий. У него есть отличные альтернативы: RPC, GraphQL, SOAP и другие. Не следует «впихивать» своё API в рамки REST или просто «прикрывать» его красивым названием, не придерживаясь никаких правил.
Концепции REST
Richardson’s Maturity Model (RMM) делит REST API на три уровня «зрелости»: Ресурс, HTTP и HATEOAS. К третьему уровню я отношусь неоднозначно, так как его сложно поддерживать. Деление на уровни помогает понять концепции, а еще его можно использовать как «пошаговую» инструкцию к рефакторингу API. Мне RMM поможет дать оценку «качества» API из ошибок в следующем разделе.
Уровень 1. Ресурс
Основное назначение REST — предоставлять данные, их еще называют ресурсами. Чтобы получить ресурс, используется его адрес URI. Вот несколько правил построения URI, чтобы предоставить удобный и понятный интерфейс для получения ресурсов:
Используйте только существительные. Мы оперируем ресурсами, а не действиями. Чтобы указать операцию, которую мы хотим провести над ресурсом, нужно использовать HTTP-методы.
Используйте множественное число. Ресурс является либо коллекцией GET /tickets, либо частью коллекции GET /tickets/123. Для удобства работы с коллекцией следует использовать множественное число.
Не используйте расширения в названии. URI должен содержать только расположение вашего ресурса, а не формат его хранения. Так лучше читабельность и поддержка сервиса.
Используйте фильтры, пагинацию, сортировку. Если нужно получить не все данные, а лишь выборку, то стоит использовать параметры запроса GET/tickets?start-date=2021-09-15&order=price,asc&page=2
Уровень 2. HTTP
Часто разработчики API «изобретают колесо», игнорируя возможности HTTP: смешивают данные, которые касаются ресурсов, и данные о самом запросе. Вот основные составляющие HTTP, в которых можно передавать информацию о запросе:
HTTP методы. Хотя REST фокусируется на ресурсе, а не на действии, нам нужно указывать способ взаимодействия с ресурсом. Для этого существуют HTTP методы: GET, POST, DELETE, PUT, PATCH, HEAD.
HTTP статусы. Это унифицированный способ определить тип ответа: успешный, ошибка из-за невалидных данных, ошибка из-за внутренних проблем сервера.
HTTP заголовки. Headers — это место, где передается вся дополнительная информация о запросе: ожидаемые типы запроса и ответа, авторизационный токен. Поддерживаются кастомные заголовки, что позволяет разработчику передавать данные, которым не место ни в URI, ни в параметрах запроса.
Уровень 3. HATEOAS
HATEOAS — последний шаг к настоящему REST. Он ослабляет связь между потребителем и API, потому что ему не нужно хранить URI. И меньше потребность в подробной документации, ведь информацию о доступных операциях можно получить напрямую из ресурса.
Общие рекомендации
REST предлагает много концепций, чтобы удачно проектировать API. От себя добавлю пару правил «хорошего тона», которых стоит придерживаться вне зависимости от архитектурного стиля.
Понятные переменные. Без них работа с API невозможна, разве что вы напрямую общаетесь с автором.
Версионность. Версионировать API можно в URI или в заголовках. Выбирайте любой способ, главное создавайте версионность. Она позволяет вносить изменение в систему самым безболезненным для потребителя путем.
Документация. Хорошая и подробная документация намного упростит жизнь потребителю, независимо от качества самого API.
Ошибки. Иногда HTTP-статусов не хватает, чтобы описать ошибку. Или нужно уточнить, в чём именно проблема. Для этого вся информация об ошибке передается в теле ответа. Сейчас нет конвенций «Как правильно отображать ошибку». Какой бы формат вы не выбрали, придерживайтесь его.
Безопасность. Есть много способов защитить данные API, но обязательный минимум — SSL/TLS и авторизация.
Кеш. Он снижает нагрузку на сервисы за вашим API и ускоряет работу.
Id запроса и ответа. Иногда нужно понять, что пошло не так, найти в логах нужный запрос, отследить цепочку вызовов. Для этого можно присваивать каждому запросу уникальный Id или принимать такой Id в заголовках запроса. Зависит от того, кому вы больше доверяете. Если у потребителя есть вопросы по работе вашего API, он предоставит вам Id интересующего запроса. А не будет объяснять на пальцах.
Stateless. В качественном API всё, что нужно для успешного ответа, передаётся в самом запросе. Оно не хранит никакой промежуточной информации между двумя запросами. Такие API проще масштабировать: любая сущность приложения может обработать любой запрос.
Ошибки при построении REST API
Какие распространенные ошибки могут совершить разработчики при написании REST API? Ниже несколько примеров. Приведенные API позиционируются именно как REST, так что и рассматривать я их буду на предмет нарушение правил этого шаблона проектирования.
Сервис адресов
Пример запроса:
GET /getAddressbyIdv4.json?id=123&lang=UA
Ошибки:
- Использование глагола в пути к ресурсу.
- Версия метода в имени ресурса.
- В имени ресурса привязка к параметру «Id».
- Расширение так же в имени ресурса.
Пример показательный: он нарушает правила первого уровня RMM. Хотя разработчики постарались полностью описать в имени ресурса назначение метода, на практике это ухудшает его читабельность. Поддерживать такой метод тяжело, поскольку любые изменения неочевидные и требуют доработок со стороны потребителя и владельца API: поднятие версии, добавление нового расширение.
Рефакторинг:
GET /v4/addresses?id=123&lang=UA Content-Type: application/json
В этом варианте сразу понятно, какой ресурс возвращает метод: в его имени нет лишней информации. Версия вынесена в отдельную часть пути, а расширение вынесено в HTTP-заголовки. Из пути к ресурсу исчезло описание, какие фильтры есть у метода. Теперь можно добавлять новые фильтры или убирать устаревшие.
Адреса клиента
Пример запроса:
GET /getClientAddress.json?id=123&lang=UA
Ошибки:
- Использование глагола в пути к ресурсу.
- Нарушение иерархии ресурсов.
- Отсутствие версионности.
- Расширение в имени ресурса.
Метод похож на предыдущий. Однако из названия понятно, что важны не только адреса, но и связь с клиентом. В остальном набор ошибок похож.
Рефакторинг:
GET /clients/1234/addresses?lang=UA Content-Type: application/json
В этот раз я указал id клиента не как параметр запроса, а добавил в путь ресурса. Так я выстроил логическую цепочку хранения ресурсов.
Получение юридических счетов
Пример запроса:
GET /allJuridicalAccounts?Id=123&session=security1&onlyOpen=Y&json=Y
Ошибки:
- Сложное имя ресурса.
- Передача авторизационной сессии как параметра запроса.
- Нестандартный формат для логического параметра.
Пример тоже похож на предыдущий. Однако вместо глагола, в пути присутствует ненужная часть «all». Есть лишняя информация о запросе: авторизационная сессия и расширение передаются в параметре запросов.
Рефакторинг:
GET /clients/123/accounts/juridical?open=true Authorization: security1 Content-Type: application/json
Всю информацию о запросе я вынес в HTTP-заголовки, чем улучшил читаемость метода. Также поменял формат логического параметра, что тоже улучшает читаемость и упрощает работу с методом. Пропала необходимость в дополнительном маппинге.
Получение информации по счёту
Пример запроса:
GET /getAccount?account=1234&session=security1&json=Y
Пример успешного ответа:
Status: 200 { "cc": "UAH", "iban": "UA1234", "err": "000000" }
Пример неуспешного ответа:
Status: 200 { "err": "000001" }
Ошибки:
- Передача авторизационной сессии как параметра запроса.
- Нестандартный формат логического параметра.
- Непонятные сокращения в именах полей.
- Нестандартный код ошибки.
Этот метод — часть того же API, что и предыдущий. Так что ошибки в запросе те же. Однако я хотел бы сфокусироваться на ответах этого метода. Его тело тяжело для понимания из-за сокращений в именах полей. Больше всего трудностей вызывает логика с ошибками: «000000» считается успешным ответом. Такую логику тяжело поддерживать, особенно если возвращаешься к ней через какое то время.
Рефакторинг:
GET /v1/accounts/1234 Authorization: security1 Content-Type: application/json
Пример успешного ответа:
Status: 200 { "currency": "UAH", "iban": "UA1234" }
Пример неуспешного ответа:
Status: 500 { "errorMessage": "Internal Error" }
Зачастую необходимо уточнить, что именно пошло не так: с помощью текста или внутреннего кода ошибки. Однако эффективнее использовать HTTP статусы, это упрощает работу с API.
Получение почты клиента
Пример запроса:
POST /rpblws_v1/rpbl/handle?format=json session: security1 action: GETMAIL { "Id": "123" }
Ошибки:
- Набор сокращений вместо имени ресурса.
- Название операции в HTTP заголовке.
- Использование POST для операции чтения.
- Нестандартная передача авторизационной сессии.
- Расширение как параметр запроса.
Один из моих «любимых» API. Идеальное пособие, как делать не нужно. Из названия метода абсолютно невозможно понять, что он делает. Подозреваю, это авторское сокращение и оно что-то говорит «избранным». Также у этого API проблема с URI: для всех методов он одинаковый, а название нужной операции передается в заголовках.
Рефакторинг:
GET /v1/clients/123/emails Authorization: security1 Content-Type: application/json
В таком варианте смещается логика с операций на ресурсы. Улучшается читаемость, вся информация о ресурсе содержится в пути, а не разбросана по телу запроса и HTTP-заголовкам. Такой метод проще поддерживать: для передачи информации о запросе используются стандартные HTTP-заголовки.
Поиск получателя
Пример запроса:
POST /findRecipient { "companyId": "1323", "serviceId": "1234A" }
Пример неуспешного ответа:
Status: 230 { "errorMessage": "Missing lang property" }
Ошибки:
- Использование Post для операции чтения.
- Использование нестандартных HTTP статусов для стандартной ситуации.
Здесь неправильно используется HTTP-метод. Обратите внимание на статус неуспешного ответа. Для стандартной валидационной ошибки используется собственный статус в интервале успешных ответов
Рефакторинг:
GET /v1/recipients?companyId=1323&serviceId=1234A
Пример неуспешного ответа:
Status: 400 { "errorMessage": "Missing lang property" }
При таком подходе проще валидировать неуспешные ответы. Большинство библиотек для работы с HTTP сразу отлавливают статусы 4xx и 5xx и оборачивают их в удобную для работы ошибку.
Данные по клиенту
Пример запроса:
POST /all { "r": [{ "Operation": "getClientRelatives", "session": "security1", "i": { "ClientId": "1234" } }] }
Ошибки:
- Один метод для работы со всеми ресурсами.
- Непонятные имена полей.
- Авторизационная сессия в теле запроса.
Это API придерживается некого JSON-RPC подхода. Однако для такого рода информационных API отлично подходит REST. Кроме того, невозможно понять без документации, что обозначают имена полей «r» или «i».
Рефакторинг:
GET /v1/clients/1234/relatives Authorization: security1 Content-Type: application/json
В сравнении с громоздким JSON, такой метод гораздо лучше читается. Видна связь между ресурсами. Такой вариант проще поддерживать: не нужно «собирать» тело запроса с кучей ненужных вложенностей.
Что вы об этом думаете?
Я хотел поделиться своими размышлениями об ошибках, которые допускают разработчики при проектировании REST API на примере конкретных API, с которыми мне приходилось работать. Очень надеюсь, что вам было полезно и кому-то статья поможет лучше понять концепции REST, для чего они нужны.
Если у вас есть примеры ошибок или же замечания по теме, пишите в комментариях — с удовольствием пообсуждаю.
Найкращі коментарі пропустити