Вы зарабатываете на информации (зачем нужен API и как его грамотно спроектировать)
Здравствуйте, меня зовут Александр Зеленин и я веб-разработчик.
Информация — основа любого приложения или сервиса.
Более 10 лет назад я общался с владельцем покер-рума, и он показал мне страницу, приносившую около 10 000$ в день. Это была совершенно банально оформленная страница. На ней не было ни стилей, ни графики. Сплошной текст, разбитый заголовками, секциями и ссылками. У меня просто не укладывалось в голове — ну как вот это может приносить такие деньги?
Секрет в том, что «вот это» было одним из первых исчерпывающих руководств по игре в покер онлайн. У страницы был PageRank 10/10 (или 9, не суть), и в поисковой выдаче это было первое, на что натыкались.
Цель вашего приложения, какое бы оно ни было — донести (получить, обработать) некоторую информацию до пользователя.
Конечно, конверсия может быть ниже, или пользователь может быть не очень доволен опытом работы с сайтом, но, если сам товар будет именно тем, что он искал — всё остальное будет малозначимо.
Я не рассматриваю магазины, продающие «на эмоциях», и покупки, о которых пользователь может потом пожалеть.
Очень часто способ доступа к этой информации уходит за пределы самого клиента игры. С помощью мобильного приложения можно проверить, не нападает ли на тебя кто, или выставить какие-нибудь товары на внутриигровой аукцион, даже не заходя в саму игру.
Конечно, хорошо использовать лицензионный контент, но если пользователь не может найти то, что искал — он уйдет и найдет это в другом месте. В интернете люди не запоминают информацию как таковую, они запоминают место, где эту информацию нашли. Поэтому, если на вашем сайте нет песен группы Х, но зато есть ссылка на страницу группы Х, где они продают свои альбомы, ваш сервис все равно в плюсе, потому что пользователь запомнил, где он взял информацию о группе Х и вернется к вам еще раз поискать информацию о группе Y.
Я работал в нескольких музыкальных проектах, и очень часто всё упиралось именно в наличие необходимых треков, несмотря на десятки терабайт данных.
В какой-то момент youtube набрал критическую массу видеозаписей и стал лидером рынка. У них был не самый удобный сайт, не самые лучшие условия. Вообще многое было не так, но именно обилие контента привлекало посетителей, и как следствие, контента становилось только больше.
Думаю, идею вы уже уловили. Примеры можно приводить бесконечно (вот ещё один: на википедию не за дизайном ходят. Более того, часть информации с википедии выводится сразу в поисковой выдаче, без открытия даже самого сайта), и если думаете, что в вашем случае это неприменимо — напишите в комментариях (или на почту / в личку), и я объясню, почему всё же применимо.
Так вот: чем бы вы ни занимались, первичной всегда будет информация. Хорошую, качественную информацию пользователи обязательно найдут и обратятся к вам.
Я расскажу, как организовать работу с информацией так, чтобы это было:
1. Масштабируемо — репликация, шардирование и т.п. настраивается БЕЗ вмешательства в работу приложения.
2. Удобно для пользователей — легко документировать, понятно как использовать.
3. Удобно для ваших разработчиков — быстрое прототипирование, возможности оптимизации только необходимого.
Данный подход не имеет смысла для вас, если у вас маленький проект с небольшим количеством компонентов и разработчиков.
Оглавление:
- Потребители информации
- Как работать с информацией (API)
- Внутренний слой API
- Внешний слой API
- Оптимизация тяжелых запросов
- Пользователи
- Масштабирование
- Кэширование
- Версионирование
- Итог
Практически в каждый из вопросов можно углубляться бесконечно, по объему подробный разбор тянет на целый цикл подобных статей. Если какая-то информация не была указана — я посчитал, что для восприятия концепта она не важна.
Если считаете, что всё же чего-то не хватает — пожалуйста, сообщите, и статья будет дополнена в соответствии с пожеланиями.
Потребителей информации можно разделить на две категории — внутренние и внешние.
Внутренние — это ваши же продукты и сервисы. Разница в том, что для «своих» API может предоставлять гораздо более широкий функционал с меньшими ограничениями. Например, те же карты гугла на собственном домене могли работать с применением webgl, и как следствие, значительно быстрее, а встраиваемые — нет (на текущий момент ситуация могла измениться, не проверял).
Внешние — конечные пользователи или продукты, не принадлежащие вашей компании. Например, для карт гугл вы являетесь внешним пользователем. Обычно доступ к информации извне сильно ограничен, и часто требуются специальные ключи доступа.
Для работы с информацией мы будем предоставлять web API. Реализовывать будем 2 слоя (внешний и внутренний). Слой подразумевает, как минимум, отдельное приложение.
Зачем нужен API?
API позволяет предоставлять данные в платформонезависимом виде. Не всегда известно, каким образом и где будут использоваться данные, и разработка API — хороший способ заявить «у нас есть информация — обращайтесь к нам».
Все примеры кодов — лишь один из многих вариантов реализаций. Это обеспечит возможность использования данных независимо от способов реализации конечного продукта (включая оффлайн приложения, при условии хотя бы единовременного выхода в сеть).
В первую очередь необходимо описать модели и коллекции данных. В случае если приложение реализуется на Javascript (nodejs на сервере), появится возможность использовать одни и те же модели и на клиентах, и на серверах.
Модель — описание некоей сущности (например, музыкальный трек): её поля, их свойства, способы доступа и предоставления информации. Модель может дублировать схему базы данных, но также может расширять её дополнительной информацией. Более того, модель может содержать информацию из нескольких таблиц/коллекций и представлять её как одну сущность. На сервере модель должна быть расширена описаниями по работе с таблицами, доступами к серверам и так далее. На клиенте модель расширяется адресами доступа к данным.
При обращении к данным модель может содержать также дополнительную метаинформацию о самом запросе (время выполнения, позиция записи в базе, связи), виртуальные поля (например, если в базе хранится path — относительный путь до файла, можно добавить виртуальное поле url, которое будет вычисляться «на лету»).
В качестве примера я приведу код, описывающий некий музыкальный сервис.
Примеры будут на Javascript, однако всё описанное применимо к любому языку. Я делал подобные вещи также на php, python и c++. Всё необходимо варьировать в зависимости от размеров проекта.
Model.extend('Track', { // Название модели
attributes: {
id: 'integer', // Поле и его тип
title: 'string',
url: 'string', // Ссылка/путь до файла
duration: 'integer',
album: 'app.model.Album.model', // Связь с другими моделями
artist: 'app.model.Artist.model'
}
})
Один пример:
Model.extend('Track', {
attributes: {
...
title: {
type: {
value: 'string',
errorText: 'Должнен иметь тип «Строка»'
},
required: {
value: true,
errorText: 'Поле обязательно для заполнения'
},
length: {
min: 5,
max: 32,
errorText: 'Длинна поля должна быть от 5 до 32 символов'
}
}
...
}
})
Коллекция — набор сущностей (обычно одинаковой природы, то есть, к примеру, музыкальные треки). Набор данных также может содержать дополнительные, относящиеся к самому набору, данные. В качестве метаинформации может быть представлено количество выбранных треков, количество оставшихся (не выбранных) треков, номер страницы, количество страниц. Виртуальным полем может быть общая продолжительность всех треков.
Model.List.extend('Track.List', { // Название коллекции
attributes: {
duration: 'virtual' // Виртуальное поле, вычисляется в момент запроса
}
}, {
duration: function(tracks) { // Вариант реализации виртуального поля
return _.reduce(tracks, function(totalDuration, track) {
return (track.duration || 0) + totalDuration;
})
}
})
Этот слой будет доступен только нашим продуктам.
Так как наши модели уже содержат много информации, мы можем предоставить доступ к данным, используя минимальное количество кода.
Model.extend('Track', { //
findOne: 'GET /track/{id}', // Найти один трек по уникальному идентификатору
update: 'PUT /track/{id}', // Обновить изменённые поля
destroy: 'DELETE /track/{id}', // Удалить из базы
create: 'POST /track' // Добавить новый трек
})
Расширяем модель только на сервере:
Model.extend('Track', {
'GET /track/top/today': function() { // Возвращает лучший трек за сегодня
var track = ...;
...
return track;
}
})
Расширяем модель только на внутреннем клиенте:
Model.extend('Track', {
findTodayTop: 'GET /track/top/today'
})
Model.extend('Track.List', {
findByArtistId: 'GET /track/byArtistId/{artist_id}' // Найти все треки, принадлежащие указаному артисту
})
На этом слое мы имеем максимальную гибкость запросов.
app.model.Track.List.findByArtistId({
format: 'json',
artist_id: 20974,
fields: [ // Поля, которые хотим получить в запросе
track, // Все поля основной модели
track.artist, // Все поля связанного музыканта
track.album.name // Название альбома, в который входит данный трек. Без других полей.
],
offset: 5, // Пропускаем первых 5 записей
limit: 10, // Получаем не более 10 записей
sort: [
'track.title': 'ASC' // Сортируем треки в алфавитном порядке
],
cache: 1800 // Кэшируем на стороне клиента на полчаса
})
В ответ получаем что-то типа:
{
"result": {
"tracks": [
...,
{
"id": 856, // Все собственные поля модели
"title": "Великолепный трек",
"url": "/какой/то/путь/до/трека",
"duration": 216,
"artist": {
"id": 20974,
"title": "Великолепный музыкант",
... // остальные поля
},
"album": {
"name": "Великолепный альбом" // только имя
}
},
...
]
},
"offset": 5, // Отступ
"count": 7, // Выбрано сейчас
"totalCount": 12 // Всего найдено
}
Не обязательно именно так вкладывать информацию. Если боитесь дубликатов (хотя, если в плане трафика, то gzip с ними отлично справляется), можно собирать в отдельные поля в начальный result.
Внешний слой доступен непосредственно конечным клиентам. Он может представлять из себя веб-сайт или просто API для сторонних разработчиков.
На внешнем слое мы НЕ предоставляем такую гибкость, как на внутреннем, а только даем доступ к базовым возможностям: основной параметр запроса, отступ, количество и т.п. Причем всё это с ограничениями.
Отчасти он является прокси до внутреннего API с рядом важных дополнений.
Сразу пример:
app.get('/api/track/:id', ..., function(req, res) {
return app.model.Track.findOne({id: req.params.id});
})
Вместо »…» делаем необходимые проверки прав, модифицируем запрос, определяем формат. Данные возвращаются в том же формате и тем же путем, как были запрошены.
Т.е. для http и json данные так и вернутся. Для socket и xml ответ будет через сокет и в xml.
Таким образом мы можем полностью контролировать, что доступно извне, как, на каких условиях и так далее.
До этого мы описывали работу с базой максимально абстрактно, и, само собой, подобные запросы будут выполняться значительно медленнее оптимизированных. С первым шагом мы обнаруживаем (с помощью профайлера, либо другим удобным вам способом), что какой-то из запросов работает медленно.
Допустим, мы обратили внимание, что медленно работает запрос, в котором выбирается трек вместе с информацией об альбоме и музыканте. Для оптимизации нам понадобится создать новый внутренний метод:
Model.extend('Track', {
'GET /track/{id}/withAdditionalData': function() {
var track = ...;
// Тут выбираем данные максимально оптимальным способом
// также используем по возможности кэш
return track;
}
})
и меняем вызов на внешнем слое ко внутреннему на представленный. Всё. Для конечного клиента ничего не поменялось, и кэш, и пути, и получаемые данные — те же, но вот работать всё стало быстрее.
Основная задача при работе с пользователями — проверять их права.
Как только пользователь авторизуется (способ не важен: куки, ключ, другой вариант), мы делаем один запрос к внутреннему слою, удостоверяя личность и получая разрешения. Далее проверки мы будем делать на внешнем слое.
Большие преимущества мы получаем именно на этапе масштабирования.
И внешний и внутренний слой API мы можем запускать в любом количестве экземпляров, разруливая нагрузку с помощью балансеров. За счет подобного разделения мы можем запустить множество приложений с внешним слоем максимально близко к конечному клиенту, получив собственную CDN сеть с кэшированием данных.
Базы данных расширяются классическими способами, зависящими от задачи.
Для внутреннего слоя мы кэшируем результаты запросов к базам данных, а на внешнем — результаты, полученные от внутреннего слоя. У конечного клиента тоже может быть кэширование.
В одном из предыдущих примеров была строка «cache: 1800» — она может предоставить кэш как на внешнем слое, запоминая результат на сервере на полчаса, так и на клиенте, сложив результат, например в localStorage (ну или другое хранилище клиента).
С развитием вашего проекта будут появляться новые методы, а какие-либо старые — уходить. Для указания версий я рекомендую, несомненно, Семантическое Версионирование. Нас особо интересует изменение мажорной версии API, без обратной совместимости. Пути доступа к API можно разделить просто: /api/{version}
Организовать файлы и поддержку разных версий можно несколькими путями, например:
1. Сделать папки v1, v2 и поместить в них весь относящийся к ним код. При модификации одной из версий API другая не затрагивается.
2. Различные репозитории, функционируют как различные приложения.
- Мы имеем полный контроль над движением данных на всех этапах.
- Разработчики довольны, им не надо ждать от команды API реализации суперхитрого метода для получения данных в определённом формате.
- Разработчики довольны, они могут оптимизировать только то, что тормозит.
- Клиенты довольны — API стабильнее и не меняет пути из-за того, что какой-то запрос был тормозной.
- Клиенты довольны — скорость доступа увеличивается за счет расположения сервера максимально близко.
Буду рад добавить в статью дополнительные разделы по вашему запросу.
Также, если хотите уточнить где-то код — напишу и приложу, спрашивайте.
Возможно, это начало большого цикла статей на различные темы (или начало уже было положено). Материала накопилось очень много, но он не систематизирован.
По планам он будет включать: видеолекции + текстовые уроки, домашние задания, самостоятельные проекты, работу с ментором, различные интенсивы, кучу кода (в формате запуска/доработки онлайн) и так далее. В качестве особенности хочу сделать «сетевой» формат, а не последовательный. Т.е. после прохождения определённых тем открываются другие, и ученик может сам выбирать, что его интересует в данный момент. По предварительным оценкам длительность обучения будет в районе полугода полноценных занятий по паре часов в день.
Понятно, что подобный проект реализуется не так быстро. Потому подойти к его разработке я хочу именно с привлечения партнёра, на чьей базе его можно будет реализовывать / продавать. Т.к. если сопутствующие вопросы я возьму на себя, то срок реализации станет совсем запредельным. Ну и плюс у разных порталов различный инструментарий, который я хочу учесть.
Я уже писал ряду проектов типа coursera с описанием своей затеи, но не получил никаких ответов. Причем, что обидно, вообще никаких, даже отказов.
Рынок изучал и уверен, что то, что реализую, более чем конкурентоспособно.
Буду рад любым предложениям о партнёрстве или советам, к кому следует обратиться.