API-интерфейсы потрясающие, но их также чрезвычайно сложно спроектировать. При создании API с нуля вам нужно правильно продумать многие детали. От базовых соображений безопасности до использования правильных методов HTTP, реализации аутентификации, принятия решения о том, какие запросы и ответы вы должны принимать и возвращать ... список можно продолжать.
В этом посте я изо всех сил стараюсь сжать все, что я знаю о том, что делает хороший API. API, который понравится вашим потребителям. Все советы не зависят от языка, поэтому они применимы к любой платформе или технологии.
1. Будьте последовательны
Я знаю, это звучит логично, но это трудно понять правильно. Лучшие API, которые я знаю, предсказуемы. Когда потребитель использует и понимает одну конечную точку, он может ожидать, что другая конечная точка работает таким же образом. Это важно для всего API и является одним из ключевых показателей того, хорошо ли разработан API и удобен ли он в использовании.
- Используйте один и то же стиль написания для полей, ресурсов и параметров (я предпочитаю `snake_case')
- Используйте имена ресурсов во множественном или единственном числе (я предпочитаю множественное число).
/users/{id}
,/orders/{id}
или/user/{id}
,/order/{id}
- Используйте одни и те же методы аутентификации и авторизации для всех конечных точек
- Используйте одни и те же HTTP-заголовки во всем API
- Например, "Api-ключ" для передачи ключа API
- Используйте одни и те же коды состояния HTTP в зависимости от типа ответа * * Например, "404", когда ресурс не может быть найден
- Используйте одни и те же методы HTTP для одних и тех же действий.
- Например, "УДАЛИТЬ" при удалении ресурса
2. Use ISO 8601 UTC dates
При работе с датой и временем API всегда должны возвращать строки в формате ISO 8601. Отображение дат в определенном часовом поясе, как правило, является задачей клиентских приложений.
{
"published_at": "2022-03-03T21:59:08Z"
}
3. Сделайте исключение для общедоступных конечных точек
Каждая конечная точка должна требовать авторизации по умолчанию. Для большинства конечных точек требуется вызов аутентифицированного пользователя, поэтому имеет смысл установить это значение по умолчанию. Если конечная точка должна вызываться публично, явно задайте для этой конечной точки разрешение несанкционированных запросов.
4. Предоставьте конечную точку проверки работоспособности
Укажите конечную точку (например, GET /health), которая определяет, является ли служба работоспособной или нет. Эта конечная точка может вызываться другими приложениями, такими как балансировщики нагрузки, для действий в случае сбоя в обслуживании.
5. Версия API
Убедитесь, что вы обновили свой API и передаете версию при каждом запросе, чтобы никакие изменения в другой версии не влияли на потребителей. Версии API могут передаваться с использованием HTTP-заголовков или параметров запроса/пути. Даже первая версия API (1.0) должна быть явно версионной.
Например:
https://api.averagecompany.com/v1/health
https://api.averagecompany.com/health?api_version=1.0
6. Примите аутентификацию по ключу API
Если API должен быть вызван третьей стороной, имеет смысл разрешить аутентификацию с помощью ключей API. Ключи API должны передаваться с использованием пользовательского HTTP-заголовка (например, Api-Key). У них должен быть срок годности, и должна быть возможность отозвать активные ключи, чтобы их можно было признать недействительными в случае их компрометации. Избегайте проверки ключей API в системе управления версиями (вместо этого используйте переменные среды).
7. Используйте разумные коды состояния HTTP
Используйте обычные коды состояния HTTP для указания успешного или неудачного выполнения запроса. Не используйте слишком много и используйте одни и те же коды состояния для одних и тех же результатов по всему API. Несколько примеров:
200
общий успех201
успешное создание400
для неправильных запросов от клиента401
для несанкционированных запросов403
для отсутствующих разрешений404
для отсутствующих ресурсов429
для слишком большого количества запросов5xx
для внутренних ошибок (их следует избегать любой ценой)
8. Используйте разумные методы HTTP
POST
для создания ресурсовPOST /users
GET
для чтения ресурсов (как отдельных ресурсов, так и коллекций)GET /users
GET /users/{id}
PATCH
для применения частичных обновлений к ресурсуPATCH /users/{id}
PUT
для применения полных обновлений к ресурсу (заменяет текущий ресурс)PUT /users/{id}
DELETE
для удаления ресурсовDELETE /users/{id}
9. Используйте понятные, простые имена
Большинство конечных точек ориентированы на ресурсы и должны быть названы именно так. Не добавляйте ненужную информацию, которая может быть получена из других источников. Это также относится к именам полей.
Хорошо
GET /users
=> Получения пользователейDELETE /users/{id}
=> Удаление пользователяPOST /users/{id}/notifications
=> Создайте уведомление для определенного пользователяuser.first_name
order.number
Плохо
GET /getUser
POST /updateUser
POST /notification/user
order.ordernumber
user.firstName
10. Используйте стандартизированные ответы на ошибки
Помимо использования кодов состояния HTTP, которые указывают результат запроса (успех или ошибка), при возврате ошибок всегда используйте стандартный ответ об ошибке, который включает более подробную информацию о том, что пошло не так. Потребители всегда могут ожидать одну и ту же структуру и действовать соответствующим образом.
// Request => GET /users/4TL011ax
// Response <= 404 Not Found
{
"code": "user/not_found",
"message": "A user with the ID 4TL011ax could not be found."
}
// Request => POST /users
{
"name": "John Doe"
}
// Response <= 400 Bad Request
{
"code": "user/email\_required",
"message": "The parameter \[email\] is required."
}
11. Возврат созданных ресурсов после POST
Хорошей идеей будет вернуть созданный ресурс после его создания с помощью POST-запроса. Это особенно важно, потому что возвращаемый созданный ресурс будет отражать текущее состояние базового источника данных и будет содержать более свежую информацию (например, сгенерированный идентификатор).
// Request: POST /users
{
"email": "jdoe@averagecompany.com",
"name": "John Doe"
}
// Response
{
"id": "T9hoBuuTL4",
"email": "jdoe@averagecompany.com",
"name": "John Doe"
}
12. Предпочитаю PATCH, а не PUT
Как упоминалось ранее, запросы на PATCH
должны применять частичные обновления к ресурсу, в то время как PUT
полностью заменяет существующий ресурс. Обычно хорошей идеей является разработка обновлений с учетом запросов на PATCH
, потому что:
* При использовании PUT
для обновления только подмножества полей для ресурса все равно необходимо передать весь ресурс, что делает его более ресурсоемким и подверженным ошибкам.
* Также довольно опасно разрешать обновлять любое поле без каких-либо ограничений
* По моему опыту, практически не существует случаев использования, когда полное обновление ресурса имело бы смысл
* Представьте себе ресурс order
, у которого есть id
и state
.
* Было бы очень опасно позволять потребителям обновлять state
order
* Изменение состояния с гораздо большей вероятностью будет вызвано другой конечной точкой (например, /orders/{id}/fulfill
)
13. Будьте как можно более конкретны.
Как описано в предыдущем разделе, обычно рекомендуется быть как можно более конкретным при проектировании конечных точек, присвоении имен полям и принятии решения о том, какие запросы и ответы принимать. Если запрос на PATCH
принимает только два поля (name
и description
), нет никакой опасности неправильного его использования и повреждения данных.
14. Используйте пагинацию
Разбейте на страницы все запросы, возвращающие набор ресурсов, и используйте одну и ту же структуру ответов. Используйте page_number
и page_size
(или аналогичные), чтобы управлять тем, какой фрагмент вы хотите извлечь.
// Request => GET /users?page_number=1&page_size=15
// Response <= 200 OK
{
"page_number": 1,
"page_size": 15,
"count": 378,
"data": [
// resources
],
"total_pages": 26,
"has_previous_page": true,
"has_next_page": true
}
15. Разрешить расширение ресурсов
Разрешить потребителям загружать связанные данные с помощью параметра запроса с именем expand
(или аналогичного). Это особенно полезно, чтобы избежать обходов и загрузить все данные, необходимые для конкретного действия, за один раз.
// Request => GET /users/T9hoBuuTL4?expand=orders&expand=orders.items
// Response <= 200 OK
{
"id": "T9hoBuuTL4",
"email": "jdoe@averagecompany.com",
"name": "John Doe",
"orders": [
{
"id": "Hy3SSXU1PF",
"items": [
{
"name": "API course"
},
{
"name": "iPhone 13"
}
]
},
{
"id": "bx1zKmJLI6",
"items": [
{
"name": "SaaS subscription"
}
]
}
]
}