Несколько недель назад, мы публично выпустили Gauges API. Несмотря на уже существующий Gauges, было не мало работы во время написания API. Необходимо подробно разобраться с деталями.
Мы допустили ошибку, занявшись подготовкой документации после того как API был практически готов. Проблема в том, что документирование ― отстой. Оставляя эту рутину на потом, когда вы уже рады бы выпустить в свет API, делает работу вдвойне сложнее. К счастью, у нас были те кто проходил это раньше.
При написании документации нашего API, мы заметили много не согласующихся моментов. Например, в некоторых местах мы возвращали хэш, а в других ― массив. Понимая проблему мы начали создавать некоторые правила.
Чтобы решить массив/хэш проблему, мы выбрали в качестве ответа всегда возвращать хэш. Это самое гибкое решение, ориентируясь на наши будущие задачи. Мы смогли ввести новые ключи, без нужды конвертировать ответ от нашего сервиса или выпуска новой версии API.
Замена массивов на хеши означала то, что нам нужно пространство имен (namespace) для массивов с ключами. Далее мы заметили, что не все имело свое пространство имен. И снова, мы придумали правило. В этом случае, все объекты верхнего уровня должны иметь пространство имен, но детям этих объектов или наборам нескольких объектов не обязательны пространства имен.
Ну вы поняли. Постоянство ― важно. Всегда следует придерживаться одного формата.
Большинство моей работы с открытым ПО было обертывание различных API. И одна вещь меня очень раздражала, это было генерирование url'ов. Каждый ресурс должен знать URL'ы важные для него. Например, пользователи в Gauges имеют несколько URL'ов, которые могут быть вызваны для получения некоторых данных:
Предыдущий JSON ― это ответ от ресурса /me. Он возвращает данные о аутентифицированном пользователе а также URL'ы для обновления себя (self), получения всей статистики (/gauges), и получения всех клиентов API (/clients). Предположим следующий ваш запрос /gauges. Каждая gauge (статистика) возвращает еще несколько адресов для получения дополнительной информации.
Мы думаем это докажет свою полезность. И увидим в будущем будет ли это хорошо работать.
И наконец, никогда не используйте to_json и его друзей из контроллера или из блоков синатры get/post/put. После того как вы начнете вызывать to_json с :methods, :except, :only, или с другими опциями, вы возможно захотите переместить это все в отдельный класс.
Для Gauges, мы зовем эти классы презентерами (presenter). Например вот упрощенная версия UserPresenter.
Ничего страшного. Простой руби класс, расположенный в app/presenters. Вот пример того, как маршрут для /me выглядит в нашем Синатра приложении.
Это простой слой представления, позволяющий легко тестировать ответы в деталях, используя юнит тестирование. И еще у нас есть один интеграционный тест, который подтверждает, что все правильно работает. Думаю этот небольшой слой ― глоток свежего воздуха.
Уверен, что ничего из выше написанного не было шокирующим или супер-вдохновляющим, но я надеюсь статья сохранит немного времени для вашего следующего публичного API.
1. Пишите документацию во время создания API
Мы допустили ошибку, занявшись подготовкой документации после того как API был практически готов. Проблема в том, что документирование ― отстой. Оставляя эту рутину на потом, когда вы уже рады бы выпустить в свет API, делает работу вдвойне сложнее. К счастью, у нас были те кто проходил это раньше.
2. Будьте постоянным
При написании документации нашего API, мы заметили много не согласующихся моментов. Например, в некоторых местах мы возвращали хэш, а в других ― массив. Понимая проблему мы начали создавать некоторые правила.
Чтобы решить массив/хэш проблему, мы выбрали в качестве ответа всегда возвращать хэш. Это самое гибкое решение, ориентируясь на наши будущие задачи. Мы смогли ввести новые ключи, без нужды конвертировать ответ от нашего сервиса или выпуска новой версии API.
Замена массивов на хеши означала то, что нам нужно пространство имен (namespace) для массивов с ключами. Далее мы заметили, что не все имело свое пространство имен. И снова, мы придумали правило. В этом случае, все объекты верхнего уровня должны иметь пространство имен, но детям этих объектов или наборам нескольких объектов не обязательны пространства имен.
{users:[{user:{...}}, {user:{...}}]} // нет
{users:[{...}, {...}]} // да
{username: 'jnunemaker'} // нет
{user: {username:'jnunemaker'}} // да
Ну вы поняли. Постоянство ― важно. Всегда следует придерживаться одного формата.
3. Предоставляйте URL'ы
Большинство моей работы с открытым ПО было обертывание различных API. И одна вещь меня очень раздражала, это было генерирование url'ов. Каждый ресурс должен знать URL'ы важные для него. Например, пользователи в Gauges имеют несколько URL'ов, которые могут быть вызваны для получения некоторых данных:
{
"user": {
"name": "John Doe",
"urls": {
"self": "https://secure.gaug.es/me",
"gauges": "https://secure.gaug.es/gauges",
"clients": "https://secure.gaug.es/clients"
},
"id": "4e206261e5947c1d38000001",
"last_name": "Doe",
"email": "john@doe.com",
"first_name": "John"
}
}
Предыдущий JSON ― это ответ от ресурса /me. Он возвращает данные о аутентифицированном пользователе а также URL'ы для обновления себя (self), получения всей статистики (/gauges), и получения всех клиентов API (/clients). Предположим следующий ваш запрос /gauges. Каждая gauge (статистика) возвращает еще несколько адресов для получения дополнительной информации.
{
"gauges": [
{
// various attributes
"urls": {
"self":"https://secure.gaug.es/gauges/4ea97a8be5947ccda1000001",
"referrers":"https://secure.gaug.es/gauges/4ea97a8be5947ccda1000001/referrers",
"technology":"https://secure.gaug.es/gauges/4ea97a8be5947ccda1000001/technology",
// ... etc
},
}
]
}
Мы думаем это докажет свою полезность. И увидим в будущем будет ли это хорошо работать.
4. Представление данных
И наконец, никогда не используйте to_json и его друзей из контроллера или из блоков синатры get/post/put. После того как вы начнете вызывать to_json с :methods, :except, :only, или с другими опциями, вы возможно захотите переместить это все в отдельный класс.
Для Gauges, мы зовем эти классы презентерами (presenter). Например вот упрощенная версия UserPresenter.
class UserPresenter
def initialize(user)
@user = user
end
def as_json(*)
{
'id' => @user.id,
'email' => @user.email,
'name' => @user.name,
'first_name' => @user.first_name,
'last_name' => @user.last_name,
'urls' => {
'self' => "#{Gauges.api_url}/me",
'gauges' => "#{Gauges.api_url}/gauges",
'clients' => "#{Gauges.api_url}/clients",
}
}
end
end
Ничего страшного. Простой руби класс, расположенный в app/presenters. Вот пример того, как маршрут для /me выглядит в нашем Синатра приложении.
get('/me') do
content_type(:json)
sign_in_required
{:user => UserPresenter.new(current_user)}.to_json
end
Это простой слой представления, позволяющий легко тестировать ответы в деталях, используя юнит тестирование. И еще у нас есть один интеграционный тест, который подтверждает, что все правильно работает. Думаю этот небольшой слой ― глоток свежего воздуха.
Уверен, что ничего из выше написанного не было шокирующим или супер-вдохновляющим, но я надеюсь статья сохранит немного времени для вашего следующего публичного API.