IMPORTANT: Per accedir als fitxer de subversion: http://acacha.org/svn (sense password). Poc a poc s'aniran migrant els enllaços. Encara però funciona el subversion de la farga però no se sap fins quan... (usuari: prova i la paraula de pas 123456)

Conceptes previs

HTTP

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

Introducció als serveis web

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

REST

Consulteu REST

REST vs Serveis web

NOTA: Entenem Serveis web com Serveis Web Tradicionals amb XML

TODO

Vegeu també JSON vs XML

Altres. Alternatives tecnològiques amb 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

Bones pràctiques i patrons de disseny

Presentació de dades

  • Json és el format més utilitzat actualment per a les APIs especialment per la seva vinculació amb Javascript
  • Laravel proporciona diferents ajudes per treballar amb Json ([1]). Vegeu també Laravel Responses:
  • Les respostes HTTP que tinguin com a contingut un array o una colecció es formataran per defecte com a Json. Per exemple: return App\User::all();
  • De fet qualsevol model si es fa un casting a String retornarà un Json: return (string) $user;
  • Utilitzar objectes:Wrapping into objects: https://laracasts.com/series/whip-monstrous-code-into-shape/episodes/20

Metadades

  • Sovint les apis no només mostren les dades en si en format json si no que sovint hi ha metadades de les pròpies dades com per exemple dates/timestamps, dades de paginació, totals, etc.
  • Aquest tema està relacionat amb la majoria dels altres temes que s'han de tenir en compte com la paginació, els codis de resposta, etc.

Paginació:

  • Mostrar totes les dades de com no sol ser una bona idea sobretot si es tracta de moltes dades.
  • Vegeu Laravel Pagination

Codis de resposta i gestió errors:

Recursos:

Metadades

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

Errors messages

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

Transformations

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:

  • Definir quins camps de la base de dades es mostraran i quins no (per exemple cal mostrar el id intern de base de dades?)
  • Definir el format dels camps i fer transformacions si cal (convertir strings a booleans, etc)

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:

Pagination

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
        }
   ]
}

API versioning

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
    });
});

Nested resources

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\UserTasksController@index                | api        |
|        | POST      | api/user/{user}/tag            | user.tag.store       | App\Http\Controllers\UserTasksController@store                | api        |
|        | GET|HEAD  | api/user/{user}/tag/create     | user.tag.create      | App\Http\Controllers\UserTasksController@create               | api        |
|        | GET|HEAD  | api/user/{user}/tag/{tag}      | user.tag.show        | App\Http\Controllers\UserTasksController@show                 | api        |
|        | PUT|PATCH | api/user/{user}/tag/{tag}      | user.tag.update      | App\Http\Controllers\UserTasksController@update               | api        |
|        | DELETE    | api/user/{user}/tag/{tag}      | user.tag.destroy     | App\Http\Controllers\UserTasksController@destroy              | api        |
|        | GET|HEAD  | api/user/{user}/tag/{tag}/edit | user.tag.edit        | App\Http\Controllers\UserTasksController@edit                 | api        | 
+--------+-----------+--------------------------------+----------------------+---------------------------------------------------------------+------------+


Recursos:

Seeders

É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:

ExceptionHandlers (findOrFail)

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:

Repository Pattern

Vegeu Repository pattern

Rate limiting

Authentication

Vegeu Token Authentication i Oauth

Testos

Desenvolupaments_APIS_web#Pagination

Recusos

Autenticació

Tenim múltiples opcions:

HTTP Basic Authentication

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:

HTTP Digest

HMAC

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

API Key

Vegeu API Key

OAuth

Vegeu OAuth

JWT

Vegeu Json Web Tokens

Content negotation

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

Llenguatges de programació. Frameworks i llibreries per al desenvolupament APIs web

Android

Vegeu:

Recursos

Javascript

Angular

Vegeu Angular i REST

PHP

Laravel

Consulteu Laravel i Laravel APIs

Code Igniter

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

codeigniter-restserver

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":"example1@example.com","fact":"Loves sw^Cming"}

Recursos:

codeigniter-restclient

Recursos:


Slim

Llista de llibreries i Frameworks

RESTful API frameworks per a PHP:

  • Laravel
  • Guzzle: web service clients
  • CodeIgniter: http://net.tutsplus.com/tutorials/php/working-with-restful-services-in-codeigniter-2/
  • Restler: consulteu Restler
  • Dave: DAVE is a minimalist, multi-node, transactional API framework written in PHP. which contains an end-to-end API test suite for TDD, a Task model, an Active Database Model, and a stand-alone development server to get you started. DAVE is an acronym that stands for Delete, Add, Edit, and View. These 4 methods make up the core functionality of many transactional web applications. The DAVE API aims to simplify and abstract may of the common tasks that these types of APIs require.
  • Epiphany: A micro PHP framework that’s fast, easy, clean and RESTful. The framework does not do a lot of magic under the hood. It is, by design, very simple and very powerful. The documentation provides a few conventions that will lead to well better code, but you’re free to use any style you’d like. According to Epiphany, the framework never dictates how you should write or structure your application.
  • FRAPI: - FRAPI is a high-level API framework that powers web apps, mobiles services and legacy systems, enabling a focus on business logic and not the presentation layer. FRAPI handles multiple media types, response codes and generating API documentation. FRAPI was originally built by echolibre to support the needs of their client’s web apps, and now it’s been open-sourced.
  • Recess: - Recess is a RESTful PHP framework that can be used by both beginner and seasoned developers. Recess is fast, light-weight, and has a very small footprint—ideal for LAMP development and drag-and-drop deployment to shared hosts. Recess is a modern framework that uses a loosely-coupled Model-View-Controller architecture designed and optimized specifically for PHP 5.
  • Slim - What began as a weekend project became a simple yet powerful PHP 5 framework to create RESTful web applications. The Slim micro framework is everything you need and nothing you don’t. Slim lets you build a complete PHP web service with only a single PHP file. Features include: RESTful routing, Named routes, Route passing, Route redirects, Route halting, Custom views, HTTP caching, Signed cookies, Custom 404 page, Custom 500 page, Error handling and Logging.
  • Tonic – Tonic is an open source less is more, RESTful Web application development PHP library, where everything useful is a resource, not a file, not a CGI script, a resource, an abstract concept of something useful that the client wants to grab hold of. Resources are located by URLs, URLs are cheap and form the universal addressing system of the Web. Tonic helps you develop Web applications that embrace the way the Web really works, enabling your applications to scale, extend and work with other systems easily.
  • Zend Framework: Zend_Rest_Server is intended as a fully-featured REST server. To call a Zend_Rest_Server service, you must supply a GET and POST methods, with a value that is the method you wish to call. You can then follow that up with any number of arguments using either the name of the argument or using arg following by the numeric position of the argument. When returning values, you can return a custom status, you may return an array with each status.

Recursos:

Clients REST

Android com a REST Client

Vegeu també:

httpie

Vegeu httpie

Curl com a rest client

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:

Troubleshooting. Resol·lució de problemes

Com es poden fer peticions POST, PUT,DELETE amb els navegadors

Resposta curta: no es pot. Es poden però instal·lar plugins o utilitzar altres eines com curl

Vegeu també

Enllaços externs