Это перевод статьи RESTful API Server – Doing it the right way (Part 1)
В 2007 Стив Джобс назвал iPhone революцией в индустрии высоких технологий, изменяющий способы работы и ведения бизнеса. Сейчас, в 2012, все больше и больше сайтов предлагают нативные клиенты для iOS и Android в качестве фронд-энд-приложений. Не все стартапы имеют финансирование для разработки приложения в добавок к основному продукту. Для увеличения скорости развития их продукта они предлагают публичный API для разработчиков, которые могут использовать его для написания приложений. Twitter, вероятно, был первой такой компанией и теперь все больше и больше компаний последовали такой же стратегии, действительно отличным путем развития экосистемы вокруг своего продукта.
Стартапы полны преобразований. Если ваш код не может переориентироваться, вы проиграете. Взяв перерыв или еще на старте, серверный код достаточно гибок, что бы адаптироваться к потребностям бизнеса. Успешными стартапами являются не те у которых присутствует хорошая идея, а те, которые реализуют ее. Успех стартапа зависит от успеха его продукта, пусть это iOS-приложение или его сервис или его API. За последние 3 года я работал над разнообразными iOS-приложениями(в основном для стартапов), использующие веб-сервисы и в этой статье, я попробовал собрать знания и показать вам лучшие практики, чтобы вы могли применить их при разработке RESTful API. Хороший RESTful API который не сопротивляется изменениям.
Целевая аудитория
Этот пост предназначен для читателей, имеющих средние и продвинутые знания о разработке RESTful API и имеющих простые знания о любом объектно-ориентированном(или функциональном) серверном языке, таком как Java/Ruby/Scala(примечание: я намеренно игнорирую PHP)
Структура и организация
Эта часть статьи довольно детальна: в первой части объясняются основы REST, во второй — документирование и управление версиями вашего API. Первая часть для новичков. Вторая — для профи. Я знаю, вы профи. Так что вот ссылка к разделу о документировании API, можно перейти прямо сейчас. Возможно это то место, с которого вы начнете читать, если решите, что не нужно тратить много времени на основы.
RESTfull-ограничения
Сервер RESTful это тот, который соответствует REST-ограничениям. Вот ссылка на статью в Википедии. Не только при разработке API, которым будут пользоваться преимущественно мобильные устройства, но и при поддержке и развитии, нужно понимать три основных ограничения. Позвольте объяснить.
Безгражданство
Первое ограничение — это безгражданство. Проще говоря, RESTful-сервер ничего не должен знать о клиенте. Клиент, с другой стороны, может поддерживать состояние контекста сервера. Другими словами, вы не должны делать так, что бы сервер помнил о состоянии мобильного устройства, использующего API.
Представим, что ваш стартап это «следующий Фэйсбук». Хороший пример, того где разработчик может сделать ошибку, это предоставление интерфейса API, который позволит мобильному устройству взять последний элемент потока(говоря о фиде Фейсбука). API обычно возвращает новые элементы, которые появились после последнего считывания. Это ведь разумно, неправда ли? Вы так «оптимизируете» передачу данных между клиентом и сервером, не так ли? И это неправильно.
Что пойдет не так в этом случае? Когда пользователь пользуется вашим сервисом с двух или трех устройств и одно устройство установило статус последнего прочтенного элемента, то другие устройства уже не смогут получить те элементы которые получило первое устройство.
Данные, возвращаемые для конкретного вызова API, не должны зависить от вызовов которые были сделаны раннее чем этот.
Верный путь оптимизации, это передача штампа времени, когда был последний вызов API(/feed?lastFeed=20120228). Есть и другой способ, более стандартный, использование заголовка HTTP Modified Since. Но пока это пропустим. Рассмотрим этот способ во второй части статьи.
Кешируемая и многоуровневая архитектура
Второе ограничение заключается в обеспечении уверенности клиента в том, что ответ может быть закеширован и использоваться в отдаче в какой то период времени, без генерации на сервере. Клиент может быть реальным мобильным клиентом или промежуточным прокси-сервером. Подробно объясню это позднее во второй главе этой статьи.
Клиент-серверное разделение интересов и единый интерфейс
RESTful-сервер должен абстрагироваться и скрывать как можно больше деталей реализации от клиента. Т.е. клиент не должен знать о том какая база данных используется на сервере и сколько баланщировщиков нагрузки сейчас активно или другие подобные вещи. Поддержание хорошоге разделения интересов помогает в расширении, когда продукт становится «вирусным».
Это вероятно три наиболее важных ограничения, которые вы должны помнить, когда разрабатываете RESTful-сервер.
REST-запросы и четыре HTTP-метода
Кэшируемое ограничение и GET-запросы
Ключевая идея — это то, что GET-метод не изменяет состояние сервера. Это означает, что ваши запросы могут быть закешированы на любом промежуточном прокси-сервере(для сокращения нагрузки). Как разработчик сервера вы не должны подвергать изменению вашу базу данных. Это противоречит философии RESTful, второму ограничению, о котором я говорил ранее. Ваш «GET» метод даже не должен делать записи в лог и не должен сохранять время последнего обращения к сервису. Если вы вносите изменения в базу данных, то это должно всегда происходить только в методах POST/PUT.
POST или PUT
В спецификации HTTP 1.1 сказано, что PUT — идемпотент((от лат. idem — такой же и potens — сильный, то есть имеющий такую же силу). Это означает, что клиент может выполнить множество PUT-запросов к одному и тому же URI и это не должно создать/изменить дублирующие записи в базе.
Операции присваивания, являются хорошим примером идемпотентных операции.
String userId = this.request["USER_ID"];
Даже если выполнить эту операцию два или три раза, ничего вредного не произойдет.
POST же, с другой стороны, не идемпотичен. Это как оператор прибавления. Вы должны использовать POST или PUT основываясь на то, какой метод должен быть: идемпотентный или нет. Если клиент «знает» URL объекта который может быть создан, то нужно использовать метод PUT. Если клиент знает URL создателя/фабрики, то нужно использовать POST.
PUT www.example.com/post/1234
Используйте PUT, если клиент знает URI, который будет создан в результате вызова. Даже если клиент вызовет PUT -метод несколько раз, ничего вредного не произойдет и не создадутся дублирующие записи.
POST www.example.com/createpost
Используйте POST-метод, если сервер создаст уникальный ключ и вернет результат обратно клиенту. Дублирующиеся запись появятся, если позднее будет запрос с такими же параметрами.
Прочтите этот ответ на Stackoverflow, чтобы узнать больше.
DELETE
DELETE — прямолинеен. Это опять идемпотент, как и PUT, и должен использоваться для удаления записи, если она существует.
REST Responses(ответы)
Ответы вашего RESTfull-сервера могут быть в XML или JSON формате. Лично я предпочитаю JSON, потому что он проще, и размер передаваемых данных меньше, чем при использовании XML-формата. Разница может быть в несколько сотен килобайт, но используя сети 3G и непостоянное соединение мобильного устройства, эти несколько сотен килобайт могут иметь огромное значение при загрузке ответных данных.
Аутентификация
Аутентификация должна проводится по протоколу https и клиент должен отправлять пароль зашифрованный криптографическим алгоритмом. Получение sha1 хэш NSString в Objective-C является довольно прямолинейным и следующий код иллюстрирует это.
- (NSString *) sha1
{
const char *cstr = [self cStringUsingEncoding:NSUTF8StringEncoding];
NSData *data = [NSData dataWithBytes:cstr length:self.length];
uint8_t digest[CC_SHA1_DIGEST_LENGTH];
CC_SHA1(data.bytes, data.length, digest);
NSMutableString* output = [NSMutableString stringWithCapacity:CC_SHA1_DIGEST_LENGTH * 2];
for(int i = 0; i <; CC_SHA1_DIGEST_LENGTH; i++)
[output appendFormat:@"%02x", digest[i]];
return output;
}
Сервер должен сравнить зашифрованный пароль с зашифрованным паролем, сохраненным на сервере ранее. В любом случае вы не должны передавать пароль в открытом виде между клиентом и сервером. В этом правиле нет исключений. В день, когда пользователь придет и узнает, что вы храните пароли в открытом виде, будет днем смерти вашего стартапа. Потерянное доверие может уже никогда не вернуться.
Спецификация RFC 2617 говорит о двух путях авторизации с HTTP-сервером. Первый это Basic Access Authentication, второй - Digest Authentication. Для внутреннего мобильного клиента использование Basic или Digest-авторизации вполне достаточно и в большинстве серверных языков, такой механизм авторизация реализован.
Если вы планируете сделать ваш API публичным, то вам следует рассмотреть oAuth, а лучше oAuth 2.0. oAuth позволяет вашим конечным пользователям размещать контент, созданный вашим приложением, с другим сторонним поставщиком обработки ключей(логин/пароль). oAuth также позволяет пользователю полностью контролировать, что сделать публичным и какие права могут быть разрешены приложонию третьей стороной.
Facebook Graph API — это крупнейшая реализация oAuth на сегодняшний день. Используя oAuth, пользователь Фейсбуку может обмениваться фотографиями с приложением третей стороны без раскрытия персональной информации и прочих деталей(логин/пароль). А также пользователь может отменить доступ для приложения третей стороны без изменения пароля.
До сих пор я говорил о основах REST. Теперь давайте погрузимся в «мясо» статьи. В последующих разделах я поговорю о лучших практиках, которым вы должны следовать в документировании, контроле версий и устаревания вашего API.
API документация
В самой худшей документация сервера, которую разработчие может написать это та, которая содержит длинный список конечных точек API, параметры, которые необходимы и соответствующие ответы от сервера. Проблема в том, что слишком сложно делать изменения данных ответа по мере развития продукта, и это становится кошмаром. У меня есть несколько предложений как сделать документацию вашего API так, чтобы разработчик клиента мог лучше понять вас. Со временем, это вам поможет стать разработчиком лучшего сервера.
Документация
Для начала, прежде чем преступить к документированию, я бы порекомендовал подумать о своем верхнем уровне моделей объектов. Подумайте о действиях, которые должны совершать эти объекты. Foursquare API документация - это хороший пример для начала. У них есть набор объектов верхнего уровня, такие как места, пользователи и т.д. Также есть действия которые могут быть выполнены над этими объектами. Как только вы узнаете объекты верхнего уровня и действия над ними, разбор конечных точек становится простым и более четким. Например, чтобы «добавить» новое место, вероятнее всего, нужно вызвать метод похожий на /venues/add
Документируйте каждый объект верхнего уровня. Далее, с помощью этих объектов верхнего уровня, документируйте запросы и ответы, а не примитивно, только исходные типы данных. Вместо того, что бы писать, что возвращается три строки: первая — идентификатор, вторая — имя, третья — описание, напишите, что это API — возвращает модель места.
Документирование параметров запроса
Давайте предположим, что у вас есть API, который, для входа использует, авторизацию Фейсбука. Вызовем api /login
Request
/login
Headers
Authorization: Token XXXXX
User-Agent: MyGreatApp/1.0
Accept: application/json
Accept-Encoding: compress, gzip
Parameters
Encoding type – application/x-www-form-urlencoded
token – “Facebook Auth Token” (mandatory)
profileInfo = “json string containing public profile information from Facebook” (optional)
Здесь profileInfo это top-level object(объект верхнего уровня). Этого достаточно, т.к. вы уже задокументировали внутреннюю структуру этого объекта. Если ваш сервер использует те же Accept, Accept-Encoding и параметры кодирования, вы можете задокументировать их отдельно, а не повторять их везде.
Документирование параметров ответа
Ответы API должны документироваться основываясь на модели объектов верхнего уровня. Цитируя тот же форсквеерсеий пример, метод /venue/#venueid# возвращает полную модель места.
Если ваша модель окажется большой или вы захотите снизить нагрузку, рассмотрите возможность создания компактной модели. Вы можете воспользоваться этим для API, которые будут возвращать список моделей объектов. Форскверовские API поступают также. Их поиск API возвращает список «компактных мест»
Обмениваетесь идеями, документируя или сообщая другим разработчикам, что Вы возвратите, когда Вы документируете свой API, используя модели объектов. Главный вывод этого раздела — это рассмотрение документации в качестве договора между вами, сервером разработчика и клиентом разработчиков(iOS/Android/Windows Phone/др.)
Причины версионности.
До мобильных приложений, в эру приложений Веб 2.0, версионность API не была проблемой. Клиент(Javascript/AJAX front-end) и сервер разворачивались одновременно, в одно и тоже время. Потребители(ваши заказчики) всегда пользовались последним фронт-энд клиентом доступа к вашей системе. Потому как, ваша компания разрабатывала и клиент и сервер, вы имели полный контроль над тем, как использовать ваш API, и всегда изменяли клиента, если что то изменилось в API на сервере. К сожалению с нативными клиентами это не возможно. Вы можете развернуть API версии 2 и думать, что все будет хорошо, но это «разорвет» старые версии вашего приложения для iOS, даже после обновления его на АппСторе. В конченом итоге это приведет к потере клиентов. Я видел очень много айфонов, в которых, более 100 приложений, ожидают обновления. У вашего приложения есть хорошие шансы стать одним из них. Вы всегда должны быть готовы к версионности API и старению. Но поддерживать ваш API не менее 3 месяцев.
Версионность.
Развертывание серверного кода в другой каталог и используя другие URLы конечных точек автоматически не означает эффективный перенос кода.
Т.е. вместо использования http://example.com/api/v1 проложению нужно будет использовать последнею наилучшую версию 2.0 http://example.com/api/v2
Когда вы производите обновления, вы почти всегда изменяете внутреннюю структуру данных и модели объектов на вашем сервере. Это подразумевает изменения в базе данных(добавление или удаление столбцов). Чтобы внести ясность, предположим, что API вашего «следующего Фейсбука» имеет вызов /feed, который возвращает «Feed»-объект.
Сегодня, в версии 1, ваш «Feed»-объект содержит ссылку на персональную картинку(avatarURL), имя(personName) -текстовое поле(feedEntryText) и временную отметку(timeStamp) новой записи.
Позднее, во второй версии, вы добавляете возможность рекламодателям продавать свою продукцию. Теперь ваш «Feed»-объект содержит новое поле, скажем, «sourceName», вместо «personalName». Теперь ваш «Feed»-объект, новое поле с именем «sourceName», которое очень уступает человеческому имени в интерфейсе. Т.е. приложение должно отображать «sourceName», вместо «personName». Поскольку для отображения в интерфейсе больше не требуется «personName», когда присутствует «sourceName», вы решили не посылать «personName» когда посылаете «sourceName». Разумнее посылать оба поля: «sourceName» и «personName». Но, друзья мои, в жизни это не всегда не так просто. Как разработчик, вы не можете отслеживать все изменения, которые когда-либо были сделаны для каждого объекта модели в своем классе. Это просто не эффективно и через 6 месяцев, вы почти забудите, почему что-то было добавлено код.
Оглядываясь назад, в веб 2.0, это не было вообще проблемой. Фронт-энед на ява скрипте обновлялся сразу же после изменения API. Однако, IOS приложения будут отключены в отличие от веб-приложений. Только пользователь может его обновить.
Я могу предложить очень элегантное разрешение этой ситуации.
Парадигма версионности URL
Во-первых, различайте несколько версий используемых урлов.
http://api.example.com/v1/feeds будет потребляться первой версией приложения для iOS, и
http://api.example.com/v2/feeds — второй версией приложения.
Хотя это звучит хорошо, вы не должны дублировать разрабатываемый вами код, после каждого изменения, затрагиваемого выходные данные. Я рекомендую это делать, только при больших изменениях. Для небольших изменений, рассмотрите версионность ваших моделей.
Парадигма версионности модели
Ранее я показал как документировать модели. Рассматривайте эту документацию как договорное соглашение между серверными и клиентскими разработчиками. Никогда не следует вносить изменения в модель, без изменения версии. Это значит, что в нашем предыдущем примере, должно быть две модели — Feed1 и Feed2. Feed2 имеет sourceName и выводит sourceName и при наличии sourceName удаляет personName. находится в Feed2 и выводит sourceName и при наличии sourceName удаляет personName. Feed1 ведет себя по прежнему так, как это было задокументировано.
В коде исполнение запроса будет выглядеть примерно так:

Вы должны перенести код создания экземпляра в класс фабричный метод. Код должен выглядеть примерно так:
Feed myFeedObject = Feed.createFeedObject("1.0");
myFeedObject.populateWithDBObject(FeedDao* feedDaoObject);
Где 1.0 или 2.0 определяется в в контроллере из строки UserAgent.
Добавление:
Вместо того, чтобы зависеть от номера версии в строке UserAgent, клиент должен отправить номер версии в заголовке Accept.
Поэтому вместо отправки
Accept: application/json
вы должны отправить
Accept: application/myservice.1.0+json
Таким образом, у вас есть возможность запросить другой вариант ответа на каждый объект RES-ресурса, который вы запрашиваете.
Спасибо читателям «hacker news», которые послали мне это.
Контроллер просит метод Feed-фабрики создать нужный feed-объект, на основе входящего запроса(все запросы в UserAgent должны выглядят как AppName/1.0) и версию клиента. При такой реализации серверного кода, *любые* изменения будут простыми. Изменяя серверную часть без нарушения существующих договоров покажется легким бризом. Просто создайте новую модель, изменив фабричный метод, который создаст новую версию модели и вы продвинетесь вперед!
С такой архитектурой модель места, для приложений 1-ой версии и 2-ой все еще могут находиться на том же сервере. Ваш контроллер будет создавать объект 1-ой версии для старых клиентов и 2-ой — для новых.
Устаревание
С парадигмой версионности модели я предположил, что устаревание вашего API упрощается. Это очень важно, когда вы сделаете ваш API публичным.
Когда вы делаете мажорные апдейты, очищайте все фабричные методы ваших моделей, основанных на бизнес-решениях.
Если, после выхода 3-й версии, вы решили больше не поддерживать iOS-приложение 1-ой версии, удалите модели и строки кода, связанные с 1-ой версией API и двигайтесь дальше.
Версионность и устаревание проходят долгий путь в обеспечении долголетия вашей компании и продукта. Они гарантируют достаточную проворность для большинства из поворотных решений. Обычно поворотным изменениям противится именно техническая команда. Такая техника должна разрешить эту проблему.
Кеширование
Следующим важным шагом для повышения производительности, на котором вы должны сосредоточиться при разработки API — это кеширование. Если вы, как и все остальные, думаете о кешировании на стороне клиента, подумайте еще раз. Во второй части я объясню, как поддерживать кэширование, основываясь на стандарты HTTP 1.1.
Обработка ошибок и интернационализация вашего API
Уведомление клиента о произошедших ошибках на сервере, так же важно как и возврат правильных данных. Об этом я расскажу в третей части.