Что такое API. Объяснение с примерами
11-12-2020Reading time ~ 9 min.Alpine.js, PHP 4462
По идее про «API» должны знать все разработчики. Переводится как программный интерфейс приложения, но поскольку для нас наибольший интерес представляет Интернет, то речь пойдёт о Web API. Под ним обычно понимают некие http-запросы между клиентом и сервером. То есть всегда есть тот, кто отправляет запрос и есть тот, кто его обрабатывает и возвращает результат.
Простой пример. Вася — верстальщик, который делает сайт и знает только HTML и JavaScript. На сайте есть страница, которая выводит список пользователей. Чтобы его отобразить, нужно вначале откуда-то получить данные.
Петя, наоборот, ничего не знает о JS и верстке, но неплохо разбирается в PHP и базах данных. То есть Петя делает так, чтобы можно было сформировать данные о юзерах, а Вася сможет их получить через простой Ajax-запрос к серверу. Петя даёт Васе URL-адрес такого запроса, http-метод и список возможных параметров. Вася указывает их в fetch()
или XMLHttpRequest()
и получает готовые JSON-данные, которые прекрасно работают с его js-скриптами.
Вот этот механизм, когда программа предоставляет интерфейс для управления и называется API.
Если рассматривать Интернет, то взаимодействие происходит через REST. Общий смысл в том, что между сайтами (читай — браузером) и сервером происходит обмен через разные http-методы. Наиболее распространены POST и GET, но при желании (и возможности) можно настроить сервер и на другие методы (PUT, DELETE и т.д.).
То есть на сегодняшний день отправить и получить запрос не составляет проблем. Но есть сложность в формате передаваемых данных.
Сейчас основной вариант — это JSON, с которым нет проблем ни со стороны PHP, ни JavaScript. Это позволяет работать со сложными данными, вроде массивов. Но так было не всегда.
Об XML-RPC
Сделаю лирическое отступление, небольшой экскурс в историю. :-)
Где-то лет 15 назад, когда в HTML был полнейший бардак, витала радужная идея в том, чтобы заменить HTML на XML. Некоторые люди, которые управляли web-стандартами, почему-то решили, что любая интернет страница — это не сколько дизайн, сколько информация, а значит подлежит жесткой структуризации. А для описания сложных структур уже тогда вовсю использовался XML. HTML как бы похож на XML, поэтому они считали, что переход на XHTML займёт не так много времени и сил.
Ну и поскольку идея была модной, то XML пихали куда только можно, включая и в механизм обмена данными. Тогда и обратили внимание на уже существующий XML-RPC от Microsoft — это те же самые post-запросы, только данные нужно было оборачивать в XML-формат.
Но на самом деле популярность этого протокола среди вебмастеров объяснялась совсем другими причинами. В то время Интернет не был таким быстрым, трафик стоил дорого, а сервера были послабее. Да и браузеры мягко говоря позволяли сделать не так много, как хотелось бы. Поэтому стали появляться программы — блог-клиенты — это самая обычная компьютерная программа, которая позволяла работать с сайтом: публиковать записи, делать рубрики, загружать картинки без обращения к браузеру.
Откуда я это знаю? Когда я занимался русским ВП, то сам сделал такую программу — WPClient (она распространялась среди моих клиентов).
Из альтернатив могу вспомнить разве что BlogJet, который был достаточно популярным, хотя и не такой мощный. Но в целом идея блог-клиентов была достаточно перспективным и интересным направлением, поэтому программы появлялись постоянно.
Обмен данными между сервером и блог-клиентом происходил по протоколу XML-RPC и это был кромешный ад!
XML — очень строгий формат: где-то не тот символ, не то экранирование — всё ломалось к чертям собачьим. Тексты для сайта набирают люди, которые могут делать ошибки, которые XML ни в какую не хотел прощать. Когда я начал делать похожий блог-клиент для MaxSite CMS, то использовал обычный и очень простой текстовый формат, чтобы держаться от XML-RPC подальше.
Кстати! Первые версии MaxSite CMS также работали через XML-RPC. Админка представляла собой простые формы, которые отправляли данные на сервер как post-запрос. Я использовал библиотеку от CodeIgniter, которая в версии 1.6.2 вдруг сломалась и я расценил это как знак, что надо это безумие прекращать.
К чему эта история? Если кто-то предлагает использовать XML-формат, бегите и не оглядывайтесь.
Подытожу про блог-клиенты одним словом: Microsoft. Удивительная способность изгадить, то к чему прикасаешься. Они выпустили свою достаточно убогую, но бесплатную версию блог-клиента Windows Live Writer. Это привело к тому, что все альтернативные программы перестали развиваться, их никто не покупал, а со временем и сама тема блог-клиентов благополучно заглохла. А с ней и XML-RPC.
JSON
Сегодня, к счастью, — это основной формат обмена данными. Мы говорим о приёме и передаче данных, а если точнее: есть запрос и есть ответ.
Запрос формируется в виде Ajax. Ответ получается в виде данных Json. Я покажу примеры на Albireo с использованием Alpine.js.
Делаем произвольную страницу в Albireo, например partner.php
. Размещаем такой начальный код:
<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js"></script> <div x-data="{users: [], url: 'https://jsonplaceholder.typicode.com/users'}" x-init="fetch(url, {method: 'GET'}).then(r => r.json()).then(d => users = d)"> <template x-for="user in users"> <div class="b-flex"> <div x-text="user.id + '.'" class="w5"></div> <div x-text="user.name" class="w20"></div> <div x-text="user.email"></div> </div> </template> </div>
В момент инициализации компонента, происходит get-запрос к сайту, который возвращает json-данные. Этот сайт — placeholder для нескольких типов данных, поэтому нам подходит на начальном этапе отладки.
Давайте изменим этот пример так, чтобы данные отдавал наш собственный сайт. Для этого нужно придумать адрес, по которому мы будем получать данные. Обычно в адресе API указывается его версия — это нужно для того, чтобы в будущем без проблем перейти на новую версию не затрагивая существующих клиентов.
Сделаем например так: сайт/api/v1/users
, а сам запрос будем делать через POST.
<div x-data="{users: [], url: '<?= SITE_URL ?>api/v1/users'}" x-init="fetch(url, {method: 'POST'}).then(r => r.json()).then(d => users = d)"> ...
Сделаем ещё одну страницу Albireo, которая будет обслуживать этот запрос, например api-users.php
. В параметрах страницы указываем адрес, метод и пустой шаблон вывода:
slug: api/v1/users method: POST layout: empty.php
Данные будем возвращать из PHP. Пусть это будет обычный массив, который отдаётся в формате Json:
$users = [ [ 'id' => 1, 'name' => 'Leanne Graham', 'email' => 'Sincere@april.biz', ], [ 'id' => 2, 'name' => 'Ervin Howell', 'email' => 'Shanna@melissa.tv', ], [ 'id' => 3, 'name' => 'Clementine Bauch', 'email' => 'Nathan@yesenia.net', ], ]; echo json_encode($users, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
Если теперь обновить страницу partner, то мы увидим уже свои данные.
Запрос через FormData()
Пока у нас простой вариант. Добавим в него какие-то дополнительные условия (параметры), чтобы сервер их мог получить. Например мы хотим передать номер пользователя для которого нужны данные.
<div x-data="{users: [], url: '<?= SITE_URL ?>api/v1/users'}" x-init="fetch(url, { method: 'POST', body: (function(){ let f = new FormData(); f.append('user', '1'); return f; })(), }).then(r => r.json()).then(d => users = d)"> <template x-for="user in users"> <div class="b-flex"> <div x-text="user.id + '.'" class="w5"></div> <div x-text="user.name" class="w20"></div> <div x-text="user.email"></div> </div> </template> </div>
Здесь я добавил поле body
, в котором данные формируются через FormData()
. С моей точки зрения это самый удобный способ, хотя и более объёмный по коду.
Чтобы принять такой запрос на сервере вполне достаточно посмотреть переменную $_POST
. Поскольку сервер отдаёт json-данные, то для отладки используем лог-файл на странице api-users.php
.
$input = $_POST; file_put_contents(BASE_DIR . '_post.log', print_r($input, true) . PHP_EOL, FILE_APPEND); ...
После обновления в _post.log
мы увидим:
Array ( [user] => 1 )
Дальше мы можем взять эти данные и делать с ними что там нужно.
Запрос через JSON
Можно данные отправить в «чистом» json-виде.
<div x-data="{users: [], url: '<?= SITE_URL ?>api/v1/users'}" x-init="fetch(url, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ user: 1 }), }).then(r => r.json()).then(d => users = d)"> <template x-for="user in users"> <div class="b-flex"> <div x-text="user.id + '.'" class="w5"></div> <div x-text="user.name" class="w20"></div> <div x-text="user.email"></div> </div> </template> </div>
Обратите внимание, что body
формируется из обычного объекта, а также добавлено поле headers
, чтобы сообщить серверу о типе данных.
Поскольку данные обрабатываются через JSON.stringify()
, то при желании можно передавать любые данные, не только объекты.
... body: JSON.stringify('bla-bla-bla'), ...
Если теперь обновить страницу, то лог вернёт пустой массив — то есть данных в $_POST
нет. Это из-за того, что такой http-запрос нужно получать тоже в «чистом» виде. Делается это через поток чтения php://input
:
$input = json_decode(file_get_contents("php://input"), true); ...
Другие http-методы
Формально мы можем использовать и другие http-методы, а не только POST и GET. Я не буду приводить новые примеры, поскольку этот вопрос рассмотрен в документации Albireo с примерами. Но на практике, если мы говорим о реальном API, то лучше всего ограничиться только POST/GET, поскольку всё остальное бессмысленное усложнение для клиентов.
Обычно GET-запрос используют там, где его адрес уже содержит дополнительные параметры. Например api/v1/users
может возвращать всех пользователей, а api/v1/users/3
— только троих (limit). То есть вы сами решаете какой адрес и что будет обозначать.
Запрос POST более сложен, но позволяет передать параметры скрыто, например id: 3
будет означать вернуть пользователя с таким идентификатором. То есть post-запрос может приходить на один адрес, но разные параметры позволяют отдавать разные данные.
Безопасность
Это уже сложный вопрос... Поскольку данные от API «гоняются» через Интернет в «открытом виде» (и доступны через браузер), то нет никаких проблем сформировать свои запросы к api-серверу и получить нужные данные. На этом базируется множество «угонов», о которых мы постоянно читаем в новостях.
Защита может строиться на разных методах. Например вводится ограничение на количество запросов с одного IP. Или можно смотреть с какого сайта был сделан запрос (referer) и если это «чужак», то отклонять его. В любом случае, если предполагается отдавать какие-то секретные данные, то лучше сто раз подумать нужно ли это делать.
Сервер-сервер
Общаться по API могут и два сервера. Например сервисы оплаты или авторизации взаимодействуют с сервером, где крутится ваш сайт на PHP. Здесь уже есть возможность использовать шифрование или другие методы защиты. Распространена практика использования т.н. токенов — это уникальный ключ, который знают только сервера. С помощью токена данные могут шифроваться, а поскольку он хранится на сервере, то узнать его постороннему достаточно сложно.
У меня не очень большой опыт работы с такими сервисами, но из всех мне больше всего понравился API ботов телеграмма. Передача данных происходит через обычный POST, но адрес формируется на основе уникального токена. Это позволяет не заморачиваться с особым шифрованием, поскольку за это отвечает сервер телеграмма. На стороне клиента вполне достаточно обычного CURL и данных для отправки в виде php-массива.
Роутер для API-сервера
Несложно догадаться, что ядром любого API будет роутер. После того, как будут определены адреса запросов, нужно будет создать для роутинга правила. Обычно это регулярки.
Например мы хотим, чтобы были запросы: api/v1/users
и api/v1/users/ЧИСЛО
, но при этом все эти запросы приходили на одну страницу api-users.php
.
В Albireo для этого используется параметр slug-pattern
(эти примеры есть в документации).
slug: api/v1/users slug-pattern: api/v1/users/([0-9]+) ...
Теперь можно использовать эти адреса для AJAX.
Чтобы получить информацию о текущем запросе, можно использовать getVal('currentUrl')
:
... file_put_contents(BASE_DIR . '_post.log', print_r(getVal('currentUrl'), true) . PHP_EOL, FILE_APPEND); ...
Будет что-то такое:
Array ( [method] => POST [url] => api/v1/users/3 [urlFull] => сайт/api/v1/users/3 )
Итого
Для создания Web API нужно организовать схему адресов и прописать правила роутинга. Если использовать Albireo Framework, то особых проблем с этим не будет. Он возьмёт на себя обслуживание http-запросов, а дальше вы уже сами решаете что с ними делать.