При розробці фреймворку Bluz переді мною постало завдання реалізувати повноцінний RESTful сервіс. Завдання з першого погляду проста, але каменем спотикання стало відсутність повноцінного RFC для реалізації оного, так що довелося покопати і поміркувати, з результатами моїх «розкопок» я і поспішаю з вами поділитися.

Якщо хочете познайомитися з першоджерелами, які я знайшов, то ось вони:

  • Книга Web API Design
  • Книга RESTful API design
  • Презентація ReSTful web services via RFC-2616
  • Сайт з REST & WOA Wiki
  • RFC 2616 – Hypertext Transfer Protocol — HTTP/1.1
  • RFC 4918 – HTTP Extensions for Web Distributed Authoring and Versioning (WebDAV)

RESTful нині став трендом при створенні API для сервісів, тобто якщо перед вами стоїть завдання реалізувати API для вашого проекту, то вам скажуть велике спасибі, якщо ви будете слідувати REST’у.

«Став трендом» — це я звичайно перебільшую, даний підхід був описаний ще в 2000-му році, і з тих пір потрохи відвойовує позиції у RPC

Я б з радістю вам почав розповідати основи основ, але тільки боюся зробити ведмежу послугу, тому вважатиму читача вже підготовленим і почну з невеликої таблиці, яка наочно зв’яже REST c CRUD (табличка з wikipedia):

Create
Read
Update
Delete
POST
GET
PUT
DELETE
/books

/books/42

Створення запису Список книг Оновити дані книг Видалити всі
Помилка Дані книги Оновити дані Видалити книгу

У цьому прикладі наочно показана структура URL яку слід успадковувати:

/:collection/
/:collection/:uid

У першому випадку ми будемо маніпулювати набором елементів, у другому — якийсь конкретної записом під певним UID.

Ну табличка нічого нового не є, давайте зупинимося детальніше на кожному з пунктів.

Читання даних

Для початку припустимо наявність у нас якоїсь бази даних користувачів:

id
firstName
lastName
1 Ivan Ivanov
2 Petr Petrov
3 Sidr Sidorov
.. .. ..

Ну, а тепер можна почати працювати з нею, насамперед отримаємо список записів з сервера:

>> GET /users/
<< HTTP/1.1 200 OK
[
{id:1, firstName:”Ivan”, lastName:”Ivanov”},
{id:2, firstName:”Petr”, lastName:”Petrov”},
{id:3, firstName:”Sidr”, lastName:”Sidorov”}
]

Якщо нам відразу всі записи не потрібні, то можна отримати дані якої-небудь конкретної записи:

>> GET /users/2
<< HTTP/1.1 200 OK
{id:2, firstName:”Petr”, lastName:”Petrov”}

Якщо запитуваної запису немає, то і відповідь буде відповідною:

>> GET /users/42
<< HTTP/1.1 404 Not Found

Кількість записів в базі даних може бути непідйомним для одного запиту, так що краще їх бити на сторінки, але для реалізації посторінкової навігації ніякої RFC не прийнято, і тут можна піти декількома шляхами:
— використовуючи заголовки призначені для роботи з Partial Content (так працює Dojo Toolkit REST Store):

>> GET /users/
>> Range: items=0-2
<< HTTP/1.1 206 Partial Content
<< Content-Range: items 0-2/3
[
{id:1, firstName:”Ivan”, lastName:”Ivanov”},
{id:2, firstName:”Petr”, lastName:”Petrov”}
]

— використовуючи GET параметри запиту, а інформації про кількість записів передавати безпосередньо в тілі відповіді:

>> GET /users/?offset=0&limit=2
<< HTTP/1.1 200 OK
{
rows:[
{id:1, firstName:”Ivan”, lastName:”Ivanov”},
{id:2, firstName:”Petr”, lastName:”Petrov”}
],
meta:{
total:3,
offset:0,
limit:2
}
}

Обидва способи мають ряд переваг і недоліків, в першому випадку це робота з заголовками, хоч це і true-way, але не дуже прозоро і перевіряти роботу не так вже й просто стає (наприклад в IDE PHPStorm є програмка, яка дозволить перевірити роботу REST сервісу ;). Другий варіант хороший всім, окрім зміни структури даних у відповіді, наприклад для backbone вам потрібно написати обробник, який дозволить працювати з такою колекцією.

Насправді, я б рекомендував не сподіватися на розробників, і в будь-якому випадку використовувати якийсь limit за замовчуванням це 10, 100 або 1 000 — залежить від розв’язуваної задачі

Що ще може нам знадобитися? Фільтрація і сортування:

>> GET /users/?filters=created(2012-01-01+)
>> GET /users/?sort=lastname-,firstname+
>> GET /users/?sort=lastname&order=desc

Тут вже як хочете так і кажете, лише дотримуйтесь одна умова — рядок GET запиту повинна легко читатися невигадливими «користувачами» вашого сервісу.

Так само, творці правильних API, рекомендують зменшити кількість трафіку, вказавши явно поля, які нам необхідні:

>> GET /users/?fields=id,lastname

Створення запису

Для створення запису використовується наш старий знайомий — метод POST:

>> POST /users/
firstName=Petrik&lastName=Petrenko
<< HTTP/1.1 201 Created
<< Location: /users/4

Тут варто звернути увагу на відповідь сервера, можна помітити код 201, і заголовок Location, який вказує нам, де нині розташований новий користувач.

Заголовок Location в даному прикладі не змусить перейти браузер на даний URL, він лише інформаційний, а що з ним робити надалі – це вже вирішувати клієнтського додатка

Ще один момент, якщо ви створюєте сервіс для програми на backbone, то дана бібліотека відправляє дані в форматі JSON, отже запит на сервер буде виглядати дещо інакше:

>> POST /users/
>> Content-Type:application/json
{“firstName”:”Petrik”,”lastName”:”Petrenko”}

На PHP подібний запит можна обробити наступним чином:

$request = file_get_contents(‘php://input’);
$data = (array) json_decode($request);

Про що ще варто згадати — це перевірка даних на сервері, якщо говорити звичною мовою, то мова піде про «валідації», і як ви вже розумієте, RFC нам тут не помічник, і треба щось вигадати, ось мій твір:

>> POST /users/
>> Content-Type:application/json
{“firstName”:”#”}
<< HTTP/1.1 400 Bad Request
<< Content-Type:application/json
{“error”:”Invalid format of ‘firstName'”,”errorCode”:12345,”errorInfo”:”http://developers.api.example.com/?error=12345″}
// or
{“errors”:{“firstName”:”Invalid format”},”errorCode”:12345,”errorInfo”:”http://developers.api.example.com/?error=12345″}

Зверну вашу увагу на наступні нюанси — це текст помилки для користувача (розробника), код помилки для розробника програми і так, сама чудова частина це посилання на документацію API, де розробник зможе знайти інформацію про те, як усунути помилку в своєму коді (або додати обробник). Цей останній пункт такий «няшный», всім рекомендую.

І не забувайте, якщо ви спробуєте передати POST’ом дані для оновлення запису, то сервер поверне помилку:

>> POST /users/3
firstName=Petrik&lastName=Petrenko
<< HTTP/1.1 400 Bad Request

Можливо відповідь 501 Not Implemented теж буде доречний, але мені здається 400-а помилка тут більше підходить, т. к. це не проблема сервера, це у клієнта помилка в коді, і лізе його код не тим методом, так і не тому адресою

Зміна даних

Для зміни даних RESTful передбачає використання двох методів PUT і PATCH, відмінності в них лише в тому, що PUT передбачає заміну записи повністю, а PATCH повинен оновлювати лише дані, які прийшли в запиті.

Почнемо з методу PUT, надсилаємо значення всіх полів:

>> PUT /users/2
firstName=Petr&lastName=Petrenko
<< HTTP/1.1 200 OK

Можна передавати дані і як JSON:

>> PUT /users/2
>> Content-Type:application/json
{“firstName”:”Petr”,”lastName”:”Petrenko”}
<< HTTP/1.1 200 OK

Якщо ж не знайдено запис:

>> PUT /users/42
firstName=Petr&lastName=Petrenko
<< HTTP/1.1 404 Not Found

Якщо нічого не змінилося (у мене є сумніви на цей рахунок):

>> PUT /users/2
firstName=Petr&lastName=Petrov
<< HTTP/1.1 304 Not Modified

Якщо ж розглядати метод PATCH, то на сервер відправляємо лише відмінності:

>> PATCH /users/2
lastName=Petrov
<< HTTP/1.1 200 OK

В API можна так само реалізувати зміна декількох записів за раз (це вірно і для PUT і для PATCH методів):

>> PATCH /users
>> Content-Type:application/json
[{“id”:1,firstName”:”Petrik”}, {“id”:2,”firstName”:”Metrik”}]
<< HTTP/1.1 200 OK

Як бути у випадку, якщо вийшло внести зміни лише до частини даних, можливо це 207 Multi-Status, але в специфікацію не вчитувався

Якщо вже я почав розмову про PHP, то в ньому з PUT і PATCH методами не все так гладко, і для отримання даних буде потрібно трошки приловчитися:

$request = file_get_contents(‘php://input’);
if ($_SERVER[‘CONTENT_TYPE’] == ‘application/x-www-form-urlencoded’) {
// plain
parse_str($request, $data);
} elseif ($_SERVER[‘CONTENT_TYPE’] == ‘application/json’) {
// or JSON
$data = (array) json_decode($request);
}
// result
print_r($data); // [‘firstName’=>’Petr’, ‘lastName’=>’Petrenko’]

Багато сервіси використовують метод PUT як псевдонім методом PATCH, так і наш «улюблений» браузер не так давно навчився працювати з методом PATCH, так що ви можете піти перевіреним шляхом, ніхто на вас образи тримати не буде

Видалення даних

О, ну тут все просто, нам потрібно метод DELETE:

>> DELETE /users/3
<< HTTP/1.1 204 No Content

Найцікавіше, але варіант видалення всіх записів за раз теж можливий:

>> DELETE /users/
<< HTTP/1.1 204 No Content

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

Пов’язані дані

Якщо у вашому проекті більше однієї сутності і вони переплелися в хитрих взаємозв’язках — значить пора додавати в API вибірку пов’язаних записів, припустимо, що наші користувачі вирішили обзавестися домашніми тваринами:

>> GET /users/1/pets
<< HTTP/1.1 200 OK
[
{id:1, petName:”Barsik”, petFamily:”Cat”},
{id:1, petName:”Tuzik”, petFamily:”Dog”}
]

Якщо нас зацікавив лише кіт:

>> GET /users/1/pets/1
<< HTTP/1.1 200 OK
{id:1, petName:”Barsik”, petFamily:”Cat”}

Виходить наступна структура URL:

/:collection/:uid/:relation
/:collection/:uid/:relation/:rid

Формат даних

Ще один важливий момент — це формат повернутих даних, за замовчуванням рекомендую використовувати формат JSON як найбільш простий і досить популярний, з його обробкою не повинно виникнути проблем ні у одного розробника. Але якщо таки виникла необхідність підтримувати кілька форматів, то треба якимось чином сказати сервера який формат нам потрібен. Тут теж кілька шляхів:
— передавати ще один параметр у запиті

>> GET /users/?format=xml

— вказувати формат як частина шляху (саме цей шлях рекомендують у книзі Web API Design)

>> GET /users.xml

— передавати заголовок Accept (мені цей варіант найбільш симпатичний)

>> GET /users
>> Accept: application/json

У разі, якщо запитуваний формат не підтримується, то слід сповістити про це розробника додатка використовуючи відповідний код:

>> GET /users
>> Accept: application/xhtml+xml
<< HTTP/1.1 406 Not Acceptable
<< Content-Type:application/json
{“accept”: [“application/json”, “application/javascript”]}

Навіть якщо ваш вибір припав на другий або третій варіант, вам, можливо, доведеться піти на компроміси, ось є формат JSONP, і спеціально для нього вам буде потрібно додавати параметр в адресний рядок, який і буде мати той самий callback, так що не все так однозначно і легко

Локалізація

Рецепт реалізації багатомовності для сервера схожий з підтримкою форматів:

>> GET /users
>> Accept-Language: da, en-gb;q=0.8, en;q=0.7
<< HTTP/1.1 406 Not Acceptable
<< Content-Type:application/json
{“accept-language”: [“ru”, “ua”]}

Як бачите, я знову використав 406-ой код помилки, при цьому текст помилки я не розписував, адже і так все має бути зрозуміло, хоча можливі й інші варіанти обробки помилок, але в будь-якому випадку, якщо використовуєте 406-й код, то в обов’язковому порядку в тілі відповіді повинна міститися інформація про те, що сервер підтримує.

Існує ще один спосіб вказівки доступного типу даних і мов — це використання заголовка Alternates, але як це буде стикуватися з 406-м заголовком мені не зовсім зрозуміло (ніби як Alternates припускає 200 код відповіді, але все ж наведу приклад:

>> GET /users
>> Accept-Language: da, en-gb;q=0.8, en;q=0.7
<< HTTP/1.1 406 Not Acceptable
<< Alternates: {“/users” 1.0 {language ru}}
<< Content-Type:application/json
{“accept-language”: [“ru”, “ua”]}

Версії API

Якщо ваш сервіс проіснує досить тривалий час, то скоріше всього його спіткає доля будь-якого іншого довгожителя, а саме — розширення і зміна функціоналу, і як наслідок у вас з’явиться кілька версій API, і тут я повністю згоден з авторами книги «Web API Design» — найкраще вказувати версію API як частину шляху:

>> GET /1.0/users
>> GET /v1.0/users

Даний підхід зручний, практичний і наочний, йому наслідують і Twitter (1й варіант) і Facebook (другий приклад).

Замість висновку

Я не хотів би тут розписувати переваги RESTful підходу, я лише виклав свої нотатки, щоб в майбутньому не забути і чого-нитку не пропустити. Я так само сподіваюся на ваші коментарі, так що якщо є чим доповнити — пишіть.

P. S.

А ще хочу порекомендувати чудову тулзу для документування та тестування RESTful API — Swagger.

P. P. S.

А є ще програма для тестування RESTful API — Postman, і її можна прикрутити до вашої CI.

P. P. P. S.

Недавное читав в ХНУРЕ лекцію про RESTful API, тепер по цій темі у мене з’явилися слайди: