Статьи

Проектирование API REST: шаблон API намерений

API-интерфейсы REST работают по HTTP, используют стандартные глаголы, такие как GETи POST, раскрывают структуру URL в здравом смысле и возвращают ресурсы в четко определенном формате, обычно JSON.

Несмотря на это простое определение, при разработке REST API существует широкая степень широты, позволяющая оптимизировать его. Не делай этого! Следуйте руководящему принципу упрощения работы для разработчиков. Обеспечьте правильный уровень детализации, сократите количество звонков, которые им нужно сделать, и документируйте это.

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

В оставшейся части этой статьи я буду говорить в первую очередь об API-интерфейсах более высокого уровня, которые в дальнейшем называются API-интерфейсом Intent .

Семантика ресурсов

В чем разница между Intent API и CRUD API? Уровень детализации. Вместо того, чтобы раскрывать детали реализации вашей фактической схемы базы данных (которая может измениться), мы вместо этого раскрываем концепции более высокого порядка того, что намерение пользователя предназначено для ваших реальных вариантов использования.

Например, в CRUD API для банка вы можете указать Ресурсы для счетов, владельцев счетов и транзакций. Вы можете разрешить вызывающим абонентам создавать учетную запись и разрешить создание транзакций. Вызывающий абонент может осуществить перевод между двумя учетными записями как две транзакции; один дебет со счета A и один депозит на счет B. Надеюсь, вы также можете сделать эти две операции атомарной транзакцией.

С Intent API вы, вероятно, вообще не разрешите создание учетной записи или ее владельца. Это, скорее всего, автономные ручные задачи, которые вы хотите, чтобы их выполняли реальные люди или, по крайней мере, ваши собственные внутренние сервисы, которые затем используют частный CRUD API. Вы также, вероятно, не хотите разрешать создание транзакции напрямую. Но, возможно, вы выставляете Ресурс переноса, а также Ресурсы на покупку и ChargeBack.

Что это тебе дает? Соответствуя намерениям пользователей — чего они на самом деле пытаются достичь — у вас есть возможность адаптировать конечную точку API только к набору параметров, которые имеют смысл для этой операции. Для перевода вам понадобятся два идентификатора учетной записи. Для покупки необходимы метаданные поставщика. Для ChargeBack вам нужен предыдущий идентификатор транзакции.

У вас также есть возможность гарантировать, что операции являются атомарными и оставить данные в допустимом состоянии. Если передача не удалась во второй части двухэтапной фиксации, вы можете откатить первую часть обратно. Вам не нужно полагаться на звонящего, чтобы сделать это правильно.

Что вы сделали, так это сняли бремя реализации бизнес-логики, характерной для вашей системы, с вызывающих и поместили ее в вашу систему (где она принадлежит).

Это правда, что это не совсем RESTful; вы выставляете глаголы как свои ресурсы. Скорее всего, вы также будете выставлять существительные (Аккаунт, Транзакция, Продавец), но любой ваш вызов, который не идемпотентен, вероятно, должен быть глаголом.

Пример этого в действии см. В GitHub API :

POST /repos/:owner/:repo/merges
{
  "base": "master",
  "head": "cool_feature",
  "commit_message": "Shipped cool_feature!"
}

Обратите внимание, что, говоря семантически, вы не создаете ресурс коммитов, который фактически представляет слияние двух веток в git. Большинство пользователей git даже не понимают, что на самом деле происходит с моделью данных для слияния; не заставляйте своих API-абонентов это понимать.

Как вы знаете, какие намерения моделировать? Я хотел бы начать с рассмотрения ваших основных пользовательских сценариев и подумать о том, какие ресурсы вы хотели бы иметь для каждого из них, имея в виду, что вы хотите минимизировать количество вызовов API, необходимых для отображения пользовательского интерфейса. Вы должны сбалансировать это со сплоченностью и разделением интересов каждого ресурса.

Структура URL

На детали реализации! Как должна выглядеть ваша структура URL? В общем, вы хотите выбрать существительные или глаголы во множественном числе или в единственном числе и придерживаться этого. Я собираюсь быть самоуверенным и скажу, что вы должны использовать множественные существительные и глаголы в единственном числе. Например:

  • /accounts — перечислить все аккаунты
  • /accounts/123 — получить аккаунт с идентификатором 123
  • /accounts/123/transactions — список транзакций, связанных с этим аккаунтом
  • /accounts/123/transactions/123 — получить определенную транзакцию внутри аккаунта
  • /transactions/123 — получить ту же Транзакцию вне контекста Аккаунта
  • /transfer — Создать перевод между двумя аккаунтами (POST)

Примечание: нет проблем с выставлением одного и того же ресурса в нескольких конечных точках. Это не СУХАЯ модель; помните, что наш руководящий принцип облегчает работу разработчиков. Возможно, они не знают, какая учетная запись связана с данной транзакцией.

Наконец, вы хотите подумать о том, какой контент отображать в корневой конечной точке /. Я видел некоторые API, которые используют эту конечную точку как возможность включать лайки в документацию для разработчиков и / или список всех конечных точек в системе.

Запросы

Самое большое решение здесь — как получить данные от абонента. Большинство API REST поддерживают параметры URL для большинства случаев использования. Если вы делаете это, убедитесь, что поддерживает POST-кодировку, это не должно быть дополнительной работой. Они хорошо работают для простых параметров ключ / значение и легко реализуются для вызывающей стороны.

Для вложенных данных у вас есть выбор поддержки тел запросов JSON или использования префиксной схемы какого-либо типа в ваших парах ключ / значение. Например, вы можете представить следующее тело JSON:

{
    "user": "Chase Seibert",
    "account": {
        "id": 1,
        "name": "foobar"
    }
}

Как пары ключ / значение ?user=Chase Seibert&account__id=1&account__name=foobar. Лично я считаю, что вызывающему в некоторых случаях это и уродливо, и сложно.

Что бы вы ни выбрали, обязательно проверяйте и уважайте тип контента вызывающих абонентов.

Метаданные и ответы

Для каждого ответа API вы хотите иметь согласованный набор метаданных, на которые могут рассчитывать вызывающие абоненты, а также согласованную общую структуру пакета. Например, вы, вероятно, хотите иметь четко определенные поля для разбивки на страницы, результатов и сообщений об ошибках. Но вы также можете включить менее очевидные элементы, такие как эхо параметров, которые пользователь передал вам. Это может быть полезно в качестве сигнала о том, что вы однозначно получили аргументы, правильно их проанализировали и что они действительны для этого вызова API.

Разбивка обычно делается, поддерживая что — то вроде limit, offsetа в sortByкачестве параметров URL. Затем вы включаете nextPageи previousPageполя в вашем ответе , которые являются абсолютными URL — адреса для этих результатов в API. Примечание: я использую camelCaseздесь против snake_case. Учитывая, что большинство потребителей API в наши дни являются либо приложениями Javascript, либо нативными мобильными приложениями (Objective-C или Java), возможно, имеет смысл использовать их соглашения и использовать верблюжий случай. Просто будь последовательным.

Сообщения об ошибках отлично подходят для разработчиков. Конечно, вы хотите, чтобы основным сигналом ошибки был правильный код состояния HTTP для этого класса ошибок.

Versioning

Возможно, вы хотите заранее продумать стратегию управления версиями API. В простейшем виде это просто префикс, подобный тому, /v1который вы добавляете к каждой конечной точке API. Идея состоит в том, чтобы заранее планировать одновременное использование нескольких поддерживаемых версий, чтобы дать разработчикам легкий переход к внесению изменений в API.

Но как вы разрабатываете свой код, чтобы иметь возможность обслуживать несколько версий? Тяжелый подход состоит в том, чтобы раскошелиться на код для каждой поддерживаемой версии. Это довольно просто и имеет то преимущество, что оно очень предсказуемо стабильно для более старых версий. Другой подход может заключаться в том, чтобы одна и та же кодовая база обслуживала несколько версий. В этом случае вам, скорее всего, потребуется сохранить несколько версий подмножества модульных тестов.

Третий гибридный подход состоит в том, чтобы работать с ветвями управления исходным кодом или с действующими виртуальными машинами или контейнерами с этим кодом. Это имеет тот недостаток, что затрудняет исправление старых версий, как для исправлений, так и для изменений инфраструктуры.

Самое главное, чтобы иметь план заранее. Я бы рекомендовал запускать как с API, так /v1и с /v0API, которые имеют некоторую обратную несовместимость, даже если это просто фиктивная конечная точка, которая удалена в версии 1.

Аутентификация

Если вы создаете публичный API, вы почти наверняка захотите использовать OAuth. Не пишите свои собственные, найдите рамки. Даже если ваш API ограничен внутренним использованием, вам следует подумать о том, чтобы включить хотя бы какой-нибудь механизм идентификации вызывающего абонента. Это может пригодиться, когда вы собираетесь производить аналитику о том, кто использует API, в дополнение к тому, что оно является обязательным условием для ограничения скорости на одного абонента.

Какой бы механизм аутентификации вы ни выбрали, вам нужно, чтобы 100% вызовов API выполнялись по протоколу HTTPS, чтобы не пропускать эти учетные данные. Даже не поддерживаю опцию HTTP.

Документация и разработчики

Почти так же важно, как семантика API, иметь превосходную, всестороннюю документацию. Не полагайтесь на автоматически сгенерированную документацию здесь. Или, по крайней мере, добавьте несколько поясняющих деталей о том, почему разработчик может захотеть использовать API, и что означает каждая часть запроса и ответа. Особенно сложно поставить себя на место человека, который не знает интимных деталей моделей в вашей системе. Запустите его третьей стороной для проверки работоспособности.

Наряду с текстовой документацией вы захотите предоставить полные, не усеченные примеры для общих запросов и ответов. Идите и убедитесь, что они напечатаны и, возможно, даже выделен синтаксис. Я также рекомендую вам распечатать действительные ответы API с сервера.

Отличный способ выставить примеры с помощью интерактивных консолей. Если вы предоставляете документацию HTML, вы даже можете сделать примеры исполняемыми и настраиваемыми. Django REST Framework является отличным примером этого.

Инструменты Python

Вот некоторые общие рамки для написания REST API в Python:

Хорошей утилитой для работы с REST API является Bunch , которая позволяет легко переводить ответы JSON API и объекты Python. Вы также можете пойти другим путем, который может быть полезен для отображения ваших объектов базы данных в JSON.

Для контроля версий ознакомьтесь с чертежами Flask .

Наконец, в зависимости от того, является ли ваш API внутренним или внешним, вы можете изучить инструменты для создания консолей API: