RESTful API Server – Делай все правильно(1 часть)

Это перевод статьи 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
  • POST
  • PUT
  • DELETE

Кэшируемое ограничение и 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

Уведомление клиента о произошедших ошибках на сервере, так же важно как и возврат правильных данных. Об этом я расскажу в третей части.

Тестирование 3G от Мегафона в Ижевске

Помнится, в году этак 2006-м я впервые подключил безлимитный интернет от Марка. Самом собой тариф был условно безлимитный, с порогом понижения скорости. Но и этого мне хватало, я был в восторге от прослушивания интернет-радио. Скорость была низкой, а порог не высокий, но это было только начало. В 2007 году мы были вынуждены сменить квартиру, и по новому адресу Марка еще не было. Был только Стрим. В то время я начинал зарабатывать фрилансом и ждать Марка не было возможности, пришлось подключится к Стриму(тогда он назывался «Ньютон»). Тариф тоже был условно безлимитным, но порог был дневным. Толи 30 МБайт в день, толи 50 МБайт…короче, немного. Но зато каждый день первые МБайты потреблял на относительно высокой скорости. Стоимость тоже точно не помню, рублей 800 вроде бы. Сейчас, конечно, условия этих тарифах кажутся смешными.

История развивается по списрали, все повторяется хоть и по новому. И вот начинают появляться безлимитные интернет-тарифы у сотовых операторов. Т.к. я пользуюсь услугами Мегафона, то и расскажу про инет от этого оператора.

Тариф у меня очень похож на старый тариф от Стрима, т.е. 30 Мбайт в день качаю на высокой скорости, а остаток — на низкой. Надо отметить, что мобильным интернетом я пользуюсь в основном для твиттора, а для этого не нужен очень быстрый инет.

Первым делом стал тестить инет на моем телефоне Nokia 5800. Speedtest.net стабильно показывал 0,5 Мбита на прием и 0,3 Мбита на отдачу. Мало, но, например через WiFI тоже не больше 1,5 Мбита.

Потом попробовал воспользоваться телефоном как 3g-модемом. Вот результат тестирования скорости.



Маловато, но опять же, подумал я, скорее всего дело в телефоне.

Раздобыл мегафоновский 3g-usb-модем. Вот тут тест показал вполне приемлемую скорость.


С таким инетом уже можно комфортно работать. Эти тесты проводил в районе центральной площади.

Скорее всего уже этой осенью я перееду жить за город(д. Старый
Бор). Никакой проводной связи там нет и в скором времени не планируется. Поэтому инет надо будет «брать из воздуха». Вариантов не много: инет от великой тройки(Мегафон, Билайн, МТС), инет от Скайлинка, ну и может быть, попробовать напавленные WiFI антенны. Впринципе, 1-2Мбита от мегафона меня устроли бы. Стал тестировать. Попробовал позвонить через Скайп, установленной на моей нокии — связь прерывистая, но говорить можно. Воткнул мегафоновский 3g-модем, вот результат теста.


Скорость не очень, особенно на выход. Для твиттора не критично, странички нормально открываются, но все таки, скорости маловато. Попробовал поработать в Google Docs, документ загружается долго, но потом, все нормально, не тормозит..

Теперь вот попробую раздобыть 3g-модем с внешней антенной и с каким-нить усилителем сигнала. Ну и других операторов попробую, хотя вряд ли они шустрее мегафона.

Жизнь по ту сторону экрана…

Компания Westernized Productions подготовила для американского производитель изделий из стекла Corning Incorporated концептуальный видеоролик, демонстрирующий возможности современных технологий производства дисплеев. В ролике продемонстрированы все возможные ипостаси стекол-экранов: от смартфонов будущего до гигантских экранов на стенах зданий.

Отсюда: Даешь дисплей в каждом стекле!

Отец мультитача о будущем интерфейсов

Билл Бакстон (Microsoft Research), создавший в 1985 году первый мультитач-планшет, рассказал «Компьютерре» о том, как различить технологии будущего в прошлом, скрывающем забытые, но до сих пор поражающие воображение идеи.

Читайте интервью

Интерент-магазин

Хошь-не хошь, но при создании интернет-магазина необходимо обратить внимание на статью «Что нам стоит магазин построить или о создании действительно прибыльного интернет-магазина» и на список 10-ти интернет магазинов с самой высокой конверсией(январь 2010)

Chrome Experements

Компания Google открыла сайт Chrome Experements, т.е. эксперименты на хроме. На самом деле на этом сайте будут собираться творения написанные на Java Script`е. Сейчас в копилке сайта 19 работ. Особенно впечатлили BallDroppings и Chromedrones.