La primera recomanació abans de començar a treballar amb REST i desenvolupament d'APIs web utilitzant REST és conèixer a fons el protocol HTTP. Vegeu doncs l'article:
HTTP
Consulteu el curs ProgramacióXML especialment les transparències de la última sessió:
http://acacha.org/svn/XMLProgramacio/moodle/curs/sessio8/transparencies/WebServices.odp http://acacha.org/svn/XMLProgramacio/moodle/curs/sessio8/transparencies/WebServices.pdf
Consulteu REST
NOTA: Entenem Serveis web com Serveis Web Tradicionals amb XML
TODO
Vegeu també JSON vs XML
Consulteu el curs ProgramacióXML especialment les transparències de la última sessió:
http://acacha.org/svn/XMLProgramacio/moodle/curs/sessio8/transparencies/WebServices.odp http://acacha.org/svn/XMLProgramacio/moodle/curs/sessio8/transparencies/WebServices.pdf
Presentació de dades
Metadades
Paginació:
Codis de resposta i gestió errors:
Recursos:
La tècnica més bàsica es retornat un json que no només porti les dades sinó que també torni les metades. El primer es tracta doncs de crear un apartat per les dades dins del Json de la resposta. Vegem un exemple amb Laravel:
$tasks = Task::all(); return Response::json([ 'data' => $tasks->toArray() ],200);
Ara ja podem afegir metadades posant-les al costat de data
https://laracasts.com/series/incremental-api-development/episodes/3
Vegem un exemple:
public function show($id) { $task = Task::find($id); if (!task) { return Response::json([ 'error' => [ 'message' => 'Task does not exist' 'code' => 40 ] ]); } }
NOTA: Es poden utilitzar exceptionhandlers i enviar excepció amb findOrFail
IMPORTANT: No confondre amb els codis de retorn HTTP són codis d'error propis de la nostra api
Exemple codis de twitter:
https://dev.twitter.com/overview/api/response-codes
Les transformacions són les encarregades de convertir les dades de la base de dades al forma adequat per a la API. Entre d'altres tenen la funcionalitat de:
Es tracta de definir uns mapeis o una transformació que agafi el model de base de dades i indiqui les transormacions:
return [ 'name' = $task['name'], 'done' = (boolean) $task['done'], 'priority' = (integer) $task['priority'], ]
Observeu que hem decidit no mostrar el id ni els timestamps.
Lo ideal es definit un objecte o objectes per cada model que s'encarreguin (tingun com a única responsabilitat SRP) de la transformació:
I utilitzar el transformador al controlador mitjançant Dependency Injection
De fet tenim llibreries com Fractal que s'utilitzen per això.
Recursos:
Laravel per defecte porta una paginador que mira el query string page per saber quina pàgina s'ha de mostrar i també automàticament pàgina en APIs Json:
Route::get('users', function () { return App\User::paginate(); });
I el resultat és:
{ "total": 50, "per_page": 15, "current_page": 1, "last_page": 4, "next_page_url": "http://laravel.app?page=2", "prev_page_url": null, "from": 1, "to": 15, "data":[ { // Result Object }, { // Result Object } ] }
Es poden utilitzar prefix de rutes en un grup de rutes per portar el tema de versions per exemple posant al fitxer api.php:
Route::group(['prefix' => 'v1'], function () { Route::get('users', function () { // Matches The "/api/v1/users" URL }); });
usertodos o usertasks
La idea dels nested resources es per exemple obtenir la llista de tasques associades només a un usuari la URL podria ser:
http://myapp.dev/api/v1/user/10/tag
Per mostrar tots els tags de l'usuari 10
Els passos a seguir són crear un controlador:
php artisan make:controller UserTasksController --resource
I afegir la ruta:
Route::resource('user.tag','UserTasksController');
Ara observeu les noves rutes:
$ php artisan route:list GET|HEAD | api/user/{user}/tag | user.tag.index | App\Http\Controllers\[email protected] | api | | | POST | api/user/{user}/tag | user.tag.store | App\Http\Controllers\[email protected] | api | | | GET|HEAD | api/user/{user}/tag/create | user.tag.create | App\Http\Controllers\[email protected] | api | | | GET|HEAD | api/user/{user}/tag/{tag} | user.tag.show | App\Http\Controllers\[email protected] | api | | | PUT|PATCH | api/user/{user}/tag/{tag} | user.tag.update | App\Http\Controllers\[email protected] | api | | | DELETE | api/user/{user}/tag/{tag} | user.tag.destroy | App\Http\Controllers\[email protected] | api | | | GET|HEAD | api/user/{user}/tag/{tag}/edit | user.tag.edit | App\Http\Controllers\[email protected] | api | +--------+-----------+--------------------------------+----------------------+---------------------------------------------------------------+------------+
Recursos:
És interessant saber crear seed amb Nested resources o recursos que depenen d'altres recursos i es pot fer amb les factories i les relacions Laravel amb:
factory(App\User::class, 50)->create()->each(function($u) { $u->tasks()->save(factory(App\Task::class)->make()); });
L'exemple anterior crea 50 usuaris amb una tasca associada.
També es pot utilitzar saveMany:
factory(App\User::class, 50)->create()->each(function($u) { $u->tasks()->saveMany([ factory(App\Task::class)->make(), factory(App\Task::class)->make(), factory(App\Task::class)->make(), ]); });
Un altre exemple:
factory(App\User::class, 20)->create()->each(function($user){ $groups = factory(App\Group::class, 5)->create()->each(function($group) { $notifications = factory(App\Notification::class, 5)->make(); $group->notifications()->saveMany($notifications); }); $devices = factory(App\Device::class, rand(2, 5))->make(); $user->groups()->saveMany($groups); $user->devices()->saveMany($devices); });
Resources:
La excepció que torna és ModelNotFoundException. Al fitxer App\Exceptions\Handler podem definir que aquesta excepció si que es reporti traient-la de dontReport i controlar com la mostrem a render:
if ($exception instanceof ModelNotFoundException) { return Response:json([ 'error' => 'Model not found exception']); }
Recursos:
Vegeu Repository pattern
Vegeu Token Authentication i Oauth
Desenvolupaments_APIS_web#Pagination
Recusos
Tenim múltiples opcions:
Les credencials s'envien en clar (o xirades si s'utilitza HTTPS)
'username:password'
El password està codificat en base64
IMPORTANT: Codificat no vol dir xifrat
GET / HTTP/1.1 Host: example.org Authorization: Basic Zm9vOmJhcg
Recursos:
HMAC
One of the downsides of basic authentication is that we need to send over the password on every request. Also, it does not safeguard against tampering of headers or body. Another way is to use HMAC (hash based message authentication). Instead of having passwords that needs to be send over, we actually send a hashed version of the password, together with more information. Let's assume we have the following credentials: username "johndoe", password "secret". Suppose we try to access a protected resource /users/foo/financialrecords. First, we need to fetch all the information we need, and concatenate this.
GET+/users/johndoe/financialrecords
Here, we just concatenate the HTTP verb and the actual URL. We could add other information as well, like the current timestamp, a random number, or the md5 of the message body in order to prevent tampering of the body, or prevent replay attacks. Next, we generate a hmac:
digest = base64encode(hmac("sha256", "secret", "GET+/users/foo/financialrecords"))
This digest we can send over as a HTTP header:
GET /users/johndoe/financialrecords HTTP/1.1 Host: example.org Authentication: hmac johndoe:[digest]
Right now, the server knows the user "johndoe" tries to acces the resource. The server can generate the digest as well, since it has all information (note that the "password" is not encrypted on the server, as the server needs to know the actual value. Hence we call this a "secret", not a "password".
Even if somebody was listening in on the conversation, it could not use the authentication information to POST data to john's financial records, or look at some other users financial records, or any other URL, as this would change the digest and the eavesdropper does not have the secret that both the server and client has.
However, the eavesdropper could access John's financial records whenever it wants since it doesn't change the digest. This is why many times more information is send over, like the current time, and a nonce:
digest = base64encode(hmac("sha256", "secret", "GET+/users/foo/financialrecords+20apr201312:59:24+123456"))
We added two extra pieces of information. The current date and a number that we only use once (nonce)
GET /users/johndoe/financialrecords HTTP/1.1 Host: example.org Authentication: hmac johndoe:123456:[digest] Date: 20 apr 2013 12:59:24
The server can reconstruct the digest again, since the client sends over the nonce and date. When the date is not in a certain range of the current servers time (say, 10 minutes), the server can ignore the message, as it probably is a replay of an earlier send message (note: either that, or the server or clients time is wrong. This is a common issue when dealing with time-limited authencations!).
The nonce is a number we only use once. If we want to access the same resource again, we MUST change this number. This means that every time we access a resource, the nonce will be different, and thus the digest will be different, even if we access the resource in the same second. This way we are sure that no replay attacks can be done. Each request is only valid once, and only once. - See more at: http://restcookbook.com/Basics/loggingin/#sthash.0dJlYtRP.dpuf
Vegeu API Key
Vegeu OAuth
Vegeu Json Web Tokens
Si utilitzem un API Rest aleshores cal utilitzar les capacitat de negociació de continguts pròpies de HTTP utilitzant la capçalera Accept. Això implica que una bona pràctica en el cas que la nostra API proporcioni tant dades en JSON com en XML utilitzem
Així mateix és important que la resposta HTTP indiqui correctament el tipus de contingut:
Content-Type: application/json; charset=UTF-8
o
Content-Type: application/xml; charset=UTF-8
En comptes d'utilitzar paràmetres de query string per indicar el tipus de contingut desitjat:
?type=json
o
?type=xml
Vegeu:
Recursos
Vegeu Angular i REST
Consulteu Laravel i Laravel APIs
Destaquem
Requeriments i qüestions prèvies
Code Igniter. Exemple amb repositori github inclòs de com crear una API REST amb codi Igniter:
Utilitzar Security Helper i String Helper
https://ellislab.com/codeigniter/user-guide/helpers/security_helper.html
Funció do_hash i random_string
Instal·lació demo:
$ cd /usr/share $ sudo git clone https://github.com/chriskacerguis/codeigniter-restserver $ cd codeigniter-restserver/ $ sudo joe application/config/config.php
Poseu:
$config['base_url'] = 'http://localhost/restserver';
Ara configurem Apache:
$ sudo joe /etc/apache2/conf.d/restserver.conf
Poseu la línia:
Alias /restserver /usr/share/codeigniter-restserver
I reinicieu Apache:
$ sudo /etc/init.d/apache2 restart
Proveu la URL:
http://localhost/restserver/
Instal·lació en un projecte ja existent:
Cal instal·lar les següents llibreries al vostre Code Igniter:
application/libraries/Format.php application/libraries/REST_Controller.php
Anem a veure un exemple de com fer-ho a ebre-escool (suposem ja teniu la app instal·lada):
$ cd /usr/share/ebre-escool/application/libraries $ sudo cp /usr/share/codeigniter-restserver/application/libraries/REST_Controller.php . $ sudo cp /usr/share/codeigniter-restserver/application/libraries/Format.php . $ cd /usr/share/ebre-escool/application/config $ sudo cp /usr/share/codeigniter-restserver/application/config/rest.php .
Ara al controlador que vulguem convertir en una API REST cal incloure un require:
require APPPATH.'/libraries/REST_Controller.php';
API Key
Al fitxer config/rest.php () poseu:
$config['rest_enable_keys'] = TRUE;
Ara si feu peticions a la URL us dirà:
{"status":false,"error":"Invalid API Key "}
Cal configurar la clau. Primer creem una taula per a les claus:
CREATE TABLE `keys` ( `id` int(11) NOT NULL AUTO_INCREMENT, `key` varchar(40) NOT NULL, `level` int(2) NOT NULL, `ignore_limits` tinyint(1) NOT NULL DEFAULT '0', `date_created` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Per crear una API key podeu utilitzar PHP:
private function generateApiKey() { return md5(uniqid(rand(), true)); }
o podeu fer un script PHP per a generar claus:
$ cat key_generator.php <?php echo md5(uniqid(rand(), true)) . "\n"; ?>
$ sudo php5 key_generator.php abfda767a957735d11d8c184a5be333a
Per provar-ho cal indicar la API key:
$ curl -X POST -H "X-API-KEY: some_key_here" http://localhost/ebre-escool/vostre-modul/vostre_controlador/vostre_recurs/id/1
Per exemple:
$ curl -H "X-API-KEY: g8314111f3d35058584b37361dbde919" http://localhost/ebre-escool/index.php/api/ebreescool/person/id/1 {"id":1,"name":"Some Guy","email":"[email protected]","fact":"Loves sw^Cming"}
Recursos:
Recursos:
RESTful API frameworks per a PHP:
Recursos:
Vegeu també:
Vegeu httpie
Nota: recomano l'ús de httpie en comptes de curl
Autorització:
Exemples:
Petició POST amb capçalera X-API-KEY:
$ curl -X POST -H "X-API-KEY: some_key_here" http://example.com/books
Credencials per HTTP Basic:
$ curl -d @credentials.json -H "Content-Type: application/json" http://192.168.100.100:35357/v2.0/tokens
GET;
No cal indicar el tipus de petició amb -X per que GET és el tipus de petició per defecte:
$ curl -i -H "Accept: application/json" http://192.168.0.165/persons/person/1
POST:
Cal utilitzar -X POST i -d o --data per passar els paràmetres. Els paràmetres es passen com si fos un Query String, és a dir múltiples paràmetres separats per &
$ curl -i -H "Accept: application/json" -X POST -d "givenName=james&sn1=Tur&sn2=Domingo" http://192.168.0.165/persons/person
PUT:
$ curl -i -H "Accept: application/json" -X PUT -d "phone=1-800-999-9999&sn1=Tur" http://192.168.0.165/persons/person/1
Si no es suporta PUT es pot fer a través de POST:
$ curl -i -H "Accept: application/json" -H "X-HTTP-Method-Override: PUT" -X POST -d "phone=1-800-999-9999" http://192.168.0.165/persons/person/1
DELETE:
$ curl -i -H "Accept: application/json" -X DELETE http://192.168.0.165/persons/person/1
Si no es suporta DELETE es pot fer a través de POST:
$ curl -i -H "Accept: application/json" -H "X-HTTP-Method-Override: DELETE" -X POST http://192.168.0.3:8090/persons/person/1
Tipus de resposta/Mime types:
Indicar el tipus de resposta, s'utilitza la capçalera Accept:
$ curl -H "Accept: application/json" http://example.com
Vegeu curl
Recursos:
Resposta curta: no es pot. Es poden però instal·lar plugins o utilitzar altres eines com curl