API Arquitectura REST - Meetup

30 sept. 2014 - transformación por parte de un proxy. 204: Respuesta vacía. 205: El servidor solicita al cliente que recargue la página. 206: Contenido parcial.
352KB Größe 30 Downloads 88 vistas
>>

API Application Programming Interface Es la abstracción de una implementación Existen muchos tipos: Sistema operativo Entornos de programación Servicios web

Arquitectura REST REST (REpresentational State Transfer) es un tipo de arquitectura de desarrollo web que se apoya totalmente en el estándar HTTP. REST nos permite crear servicios compatibles con cualquier dispositivo o cliente HTTP, por lo que es mucho más sencillo que antiguas alternativas, como SOAP y XML-RPC. REST se definió en el 2000 por Roy Fielding, coautor de la especificación HTTP. Cliente-Servidor Interfaz de comunicación que abstrae para el cliente el funcionamiento interno del servidor. Sin estado Ninguna información de contexto del cliente se almacena en el servidor (sesiones, cookies…). Cada petición del cliente contiene toda la información necesaria para que el servidor sea capaz de preparar la respuesta. Cacheable El cliente puede querer cachear las respuestas, por lo que estas deben ser cacheables. Es decir, debe incluirse en los headers de las respuestas la cabecera Last-Modified y soportar la recepción de la cabecera If-Unmodified-Since en las peticiones GET del cliente.

Interfaz uniforme Identificación de recursos Manipulación de recursos mediante su representación Mensajes autodescriptivos Hipermedia como motor de estado Sistema de capas El servidor debe seguir una arquitectura de capas, en la que cada capa conoce únicamente la capa inmediatamente inferior. De esta forma, la capa superior (interfaz de interacción) abstrae el comportamiento interno. El cliente no debe conocer nada sobre la implementación del servidor. Código en demanda Es el único requisito opcional. Se provee al cliente con código capaz de manejar la respuesta del servidor. El mejor ejemplo de código en demanda son los scripts de JavaScript que contienen las páginas web. Para que un servicio sea considerado RESTful debe cumplir una serie de requisitos: 1. Uso correcto de URIs 2. Uso correcto de HTTP 3. Implementación de Hypermedia

Uso correcto de URIs Las URL (Uniform Resource Locator) son un tipo de URI (Uniform Resource Identifier) que permiten identificar de forma única un recurso. Tienen la forma: ://[:puerto]/.

Existen una serie de reglas a la hora de nombrar recursos: El nombre de un recurso no debe implicar una acción. Las acciones ya se especifican mediante los verbos de HTTP. Deben ser únicas. No debemos tener más de una URI para identificar un mismo recurso.

Deben ser independientes del formato. /people/432.pdf no sería correcto. Deben mantener una jerarquía lógica, de lo más amplio a lo más concreto. Incorrecto: /facturas/998/clientes/232 Correcto: /clientes/232/facturas/998 Los filtrados de información de un recurso no se hacen en la URI, sino utilizando parámetros HTTP: Incorrecto: /facturas/orden/desc/fechadesde/2007/pagina/2

Correcto:

/facturas?fecha-

desde=2007&orden=DESC&pagina=2

Uso correcto de HTTP Cualquier desarrollador web debe conocer bien la especificación HTTP. Los siguientes RFC (Request For Comments) contienen la especificación completa: RFC 7230 Message syntax and routing (https://tools.ietf.org/html/rfc7230) RFC 7231 - Semantics and Content (https://tools.ietf.org/html/rfc7231) RFC 7232 - Conditional requests (https://tools.ietf.org/html/rfc7232) RFC 7233 - Range requests (https://tools.ietf.org/html/rfc7233) RFC 7234 - Caching (https://tools.ietf.org/html/rfc7234) RFC 7235 - Authentication (https://tools.ietf.org/html/rfc7235) RFC 7236 Authentication Scheme Registrations (https://tools.ietf.org/html/rfc7236) RFC 7237 - Method Registrations (https://tools.ietf.org/html/rfc7237) Aunque podemos resumirla en 3 aspectos clave: Métodos HTTP Códigos de estado Formatos de contenido

Métodos HTTP HTTP pone a nuestra disposición 5 métodos o "verbos":

GET: Para leer un recurso HEAD: Para leer metainformación de un recurso POST: Para crear un recurso PUT: Para editar un recurso DELETE: Para eliminar un recurso PATCH: Para editar partes de un recurso

Códigos de estado Para que un servicio pueda ser considerado RESTful es vital que haga un uso correcto de los códigos de estado (http://es.wikipedia.org/wiki/Anexo%3aC%C3%B3digos_de_estado_HTTP) que ofrece HTTP. Existe un código preestablecido para cada posible situación a la que nuestro servicio tenga que enfrentarse. Estos códigos se agrupan de la siguiente forma (sólo se enumeran algunos): 1XX: Respuestas informativas. Se utilizan cuando un cliente quiere

saber si su petición será aceptada o rechazada por los headers antes de enviar al servidor todo el cuerpo de la misma (si es de gran tamaño). 100: La petición será aceptada (continue) 101: Conmutando protocolos 102: Procesando 2XX: Peticiones correctas. La petición fue recibida correctamente,

entendida y aceptada. 200: OK 201: Elemento creado 202: Petición aceptada, pero aún se está procesando. Podría no

completarse si se produce algún error 203: Información no autoritativa. La respuesta ha recibido alguna transformación por parte de un proxy 204: Respuesta vacía 205: El servidor solicita al cliente que recargue la página 206: Contenido parcial. Utilizado para dividir una descarga o interrumpirlas y reanudarlas 207: Estado múltiple. El cuerpo es un XML con subcódigos de estado, dependiendo de las sub-peticiones hechas

3XX: Redirecciones. El cliente tiene que realizar una acción adicional.

No está permitido hacerlo más de 5 veces seguidas: bucle infinito de redirección. 300: Se ofrecen distintas posibilidades al cliente. Ejemplo:

distintos formatos de vídeo. 301: Movido permanentemente. 302: Movido temporalmente. Es el ejemplo más popular de redirección, así como un ejemplo de contradicción del estándar. 303: See other. La respuesta a la petición se encuentra haciendo GET a otra URI. Muchas veces se utiliza 302 como si fuera 303. 304: Indica que el recurso no ha sido modificado desde la última vez que se pidió. Ahorra ancho de banda. 307: Redirección temporal. Como 303 pero debe soportar cualquier método (no solo GET). 4XX: Errores del cliente. La solicitud es incorrecta o no puede

procesarse. 400: Solicitud incorrecta. 401: No autorizado para acceder al recurso en este momento (o

la autorización ha fallado). 403: No autorizado para acceder al recurso. La autorización se ha realizado correctamente pero el acceso es denegado. 404: Recurso no encontrado. 405: Método (GET, POST...) no permitido para la URI utilizada. 406: Los formatos aceptados por el cliente (header Accept) no están disponibles. 410: Indica que el recurso ya no está disponible ni lo estará (pero lo ha estado en el pasado). En ocasiones se utiliza erróneamente 404 en lugar de 410. 423: Recurso bloqueado. 5XX: Errores del servidor. El servidor falló al completar una solicitud

aparentemente válida. 500: Error interno, genérico, ajeno a la naturaleza del propio

servidor web. 501: Funcionalidad no implementada. 503: Servicio no disponible en este momento. 505: Versión de HTTP no soportada.

509: Límite de ancho de banda excedido (no es un código

oficial, pero es ampliamente utilizado). De esta forma conseguimos: Ofrecer una interfaz estándar Cualquier cliente puede hacer uso de nuestra API sin tener que hacer un esfuerzo adicional para integrarse con nosotros No tenemos que preocuparnos de mantener nuestros propios mensajes de estado Error común: 1. 2. 3. 4. 5. 6. 7. 8. 9.

>> PUT /people/123 Status Code 200 Content: { success: false, code: 734, error: "datos insuficientes" }

Formatos de contenido Para indicar los tipos de formato aceptados, el cliente debe especificar el header Accept. La API RESTful debe devolver el resultado en el primer formato disponible, o un error 406 si ninguno de los formatos aceptados está disponible. 1. GET /people/1234 2. Accept: application/epub+zip, application/pdf, application/json

En la respuesta se debe indicar el header Content-Type para indicar el formato del cuerpo. 1. Status Code 200 2. Content-Type: application/pdf 3. ...

Existen muchos tipos de contenido: application/pdf, audio/mp4, image/gif, multipart/form-data, text/plain, etc. Además se pueden utilizar tipos personalizados para la implementación hypermedia.

Implementación Hypermedia La idea de Hypermedia es sencilla: conectar unos recursos con otros mediante enlaces. Por ejemplo: 1. 2. 1234 3. Pending 4. 5. 6. http://example.com/api/pedido/1234/factura 7. 8. 9.

El cliente debe entender la información, de forma que sepa separar el recurso en sí con la información para enlazarla con otros recursos. Para ello se utilizan los headers Accept y Content-Type. Es una forma de que el cliente y el servicio sepan que están utilizando el mismo idioma. Este idioma debe especificarse en algún documento que el cliente de la API debe conocer. HAL (Hypertext Application Language) (https://tools.ietf.org/html/draft-kellyjson-hal-06) es un ejemplo de especificación de representación de recursos utilizando JSON. El tipo de contenido es application/hal+json: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18.

Request: GET /pedido/1234 Accept: application/hal+json, text/xml Response: Status Code 200 Content-Type: application/hal+json Content: { id: 1234, status: 'Pending', _links: { factura: { href: 'http://example.com/api/pedido/1234/factura', type: 'application/pdf' } } }

¿Para qué sirve Hypermedia? Para automatizar la utilización de APIs sin que haya interacción humana. También para no tener que conocer la ubicación exacta de todos los recursos.

Versionado de una API REST Las APIs cambian rápido Los clientes se adaptan despacio Debemos dar compatibilidad hacia atrás Versionado de la API Indicando la versión en la URL Se trata de la opción más aconsejada, aunque tiene el inconveniente de que un cliente que quiera actualizarse tiene que cambiar todas las URLs. Ejemplo: Twitter. https://api.twitter.com/1.1/statuses/user_timeline.json

Indicando la versión como parámetro de la URL Tiene el mismo inconveniente de tener que cambiar las URLs para actualizarse. Ejemplo: YouTube. http://gdata.youtube.com/feeds/api/videos? q=surfing&caption&v=2

Indicando la versión en el header Accept-Version Tiene la ventaja de que las URLs no cambian. Nuestro servidor puede, por ejemplo, separar con un proxy las peticiones de cada versión a una máquina o endpoint diferente.

APIs REST con Node.js Node.js es una plataforma de ejecución de JavaScript basada en eventos que sigue un modelo de I/O no bloqueante. Un servidor HTTP básico tiene el siguiente aspecto:

1. var http = require('http'); 2. http.createServer(function (req, res) { 3. res.writeHead(200, {'Content-Type': 'text/plain'}); 4. res.end('Hello World\n'); 5. }).listen(1337, '127.0.0.1');

Es una plataforma especialmente adecuada para la construcción de APIs REST por algunos motivos: JavaScript es perfecto para APIs JSON. El serializado/deserializado es nativo. Normalmente las APIs ofrecen peticiones pequeñas y de poca necesidad computacional, el caso de uso perfecto para Node.js, ya que durante un proceso intenso de cómputo el bucle de eventos no se procesa (cuando se ejecuta en un único proceso). NodeJS también funciona muy bien con long polling (http://es.wikipedia.org/wiki/Tecnolog%C3%ADa_Push#Long_polling), ya que no necesita crear un hilo para cada petición, como por ejemplo hacen Python o Ruby. Se pueden hacer APIs que exploten esta característica, si algunas peticiones requieren un procesamiento largo y el cliente quiere enterarse del progreso. Una API REST sencilla, para la gestión básica de un listado de usuarios, podría implementarse en NodeJS utilizando símplemente el módulo http. Supongamos la siguiente lista de usuarios: 1. var people = [ 2. {dni: 111, name: 'Pepe', age: 30}, 3. {dni: 222, name: 'Jose', age: 20}, 4. {dni: 333, name: 'Carlos', age: 25} 5. ];

Utilizando el módulo http, sobre el servidor que hemos visto antes, podemos implementar unas operaciones básicas: GET /people GET /people/ POST /people/ PUT /people/ DELETE /people/

El código sería como sigue:

1. var http = require('http'); 2. 3. http.createServer(function (req, res) { 4. 5. if (req.method === 'GET') { 6. 7. if (req.url === '/people') { 8. res.writeHead(200, {'Content-Type': 'application/jso n'}); 9. res.end(JSON.stringify(people)); 10. return; 11. } 12. 13. if (req.url.indexOf('/people/') === 0) { 14. var person = people.filter(function (p) { 15. return (p.dni == req.url.replace('/people/', '') ); 16. })[0]; 17. if (!person) { 18. res.writeHead(404); 19. res.end(); 20. return; 21. } 22. res.writeHead(200, {'Content-Type': 'application/jso n'}); 23. res.end(JSON.stringify(person)); 24. return; 25. } 26. 27. } else if (req.method === 'POST') { 28. 29. if (req.url.indexOf('/people/') === 0) { 30. (function () { 31. var body = ''; 32. req.on('data', function (data) { 33. body += String(data); 34. }); 35. req.on('end', function () { 36. people.push(JSON.parse(body)); 37. res.writeHead(201); res.end(); 38. }); 39. }()); 40. } 41. 42. } else if (req.method === 'PUT') { 43. 44. if (req.url.indexOf('/people/') === 0) { 45. (function () { 46. var body = ''; 47. req.on('data', function (data) { 48. body += String(data); 49. }); 50. req.on('end', function () { 51. body = JSON.parse(body);

52. 53.

people = people.map(function (p) { if (p.dni == req.url.replace('/people/', '')) {

54. 55. 56. 57. 58. 59. 60. 61. 62. 63. 64. 65. 66. 67.

return body; } return p; }); res.writeHead(200); res.end(); }); }()); } } else if (req.method === 'DELETE') { if (req.url.indexOf('/people/') === 0) { people = people.filter(function (p) { return (p.dni != req.url.replace('/people/', '') );

68. }); 69. res.writeHead(200); res.end(); 70. } 71. } 72. 73. }).listen(3000);

El código no es muy complejo ni extenso. No obstante, existen algunos módulos especialmente diseñados para la creación de aplicaciones y servicios web.

Módulos de Node.js para construir APIs REST A continuación se presentan algunos de los más populares frameworks de apoyo al desarrollo de APIs REST para Node.js.

Express (http://expressjs.com/) Inspirado en Sinatra (http://es.wikipedia.org/wiki/Sinatra_%28software%29), un popular framework de aplicaciones web para Ruby. Del mismo modo que Sinatra, en lugar de seguir el popular patrón MVC se centra en permitir la creación de aplicaciones web con el mínimo esfuerzo. El pequeño servicio REST listado anteriormente, con Express se simplificaría de forma considerable:

1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51.

var http = require('http'); var express = require('express'); var bodyParser = require('body-parser'); var app = express(); app.use(bodyParser.json()); var people = [ {dni: 111, name: 'Pepe', age: 30}, {dni: 222, name: 'Jose', age: 20}, {dni: 333, name: 'Carlos', age: 25} ]; app.get('/people', function (req, res) { res.send(people); }); app.get('/people/:dni', function (req, res) { var person = people.filter(function (p) { return p.dni == req.params.dni; })[0]; if (!person) { return res.send(404); } res.send(person); }); app.post('/people/:dni', function (req, res) { people.push(req.body); res.send(201); }); app.put('/people/:dni', function (req, res) { people = people.map(function (p) { if (p.dni == req.params.dni) { return req.body; } return p; }); res.send(200); }); app.delete('/people/:dni', function (req, res) { people = people.filter(function (p) { return p.dni != req.params.dni; }); res.send(200); }); var server = http.createServer(app); server.listen(3000);

Nótese la simplificación en cuanto al manejo de parámetros en la URI y las respuestas al cliente.

Restify (http://mcavage.me/node-restify/) A diferencia de Express, que está diseñado para construir aplicaciones web y añade soporte para renderizado de vistas, Restify es un framework diseñado para crear servicios REST, exclusivamente. De esta forma es más ligero que Express. El código escrito para Restify es prácticamente idéntico al escrito para Express, pero Restify ofrece algunas facilidades muy útiles para nuestras APIs: Rutas versionadas, por medio del header Accept-Version: 1. 2. 3. 4. 5. 6. 7. 8.

var restify = require('restify'); var server = restify.createServer(); var PATH = '/hello/:name'; server.get({path: PATH, version: '1.1.3'}, function (req, r es) { res.send('hello: ' + req.params.name); }); server.get({path: PATH, version: '2.0.0'}, function (req, r es) { res.send({hello: req.params.name}); });

9. 10. 11. 12. server.listen(8080);

Authorization: autenticación básica con login y password o mediante HTTP Signature (https://github.com/joyent/node-http-signature). Gzip response: comprime las respuestas. Peticiones condicionales con eTag: para el cacheo de recursos. Soporte JSONP: para paliar las limitaciones del cross-origin. Throttling: límites de peticiones en tiempo, por usuario, IP, ruta, etc.

Koa (http://koajs.com/)

Se trata de una evolución de Express, basada en el uso global de generadores (https://developer.mozilla.org/enUS/docs/Web/JavaScript/Reference/Statements/function%2a). Al contrario que Express, no incorpora ningún middleware de serie. Esto quiere decir que los objetos request y response originales del módulo http de Node.js no se alteran, sino que se trabaja con un objeto auxiliar por petición: el "contexto": 1. app.use(function *(){ 2. this; // is the Context 3. this.request; // is a koa Request 4. this.response; // is a koa Response 5. this.req; // original http request 6. this.res; // original http response 7. });

El pequeño servicio REST de ejemplo con el que estamos trabajando podría escribirse así con Koa: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30.

var koa = require('koa'); var route = require('koa-route'); var parseBody = require('co-body'); var app = koa(); var people = [ {dni: 111, name: 'Pepe', age: 30}, {dni: 222, name: 'Jose', age: 20}, {dni: 333, name: 'Carlos', age: 25} ]; app.use(function *(next) { if (['POST', 'PUT'].indexOf(this.method) === -1) { return yield next; } this.body = yield parseBody(this); yield next; }); app.use(route.get('/people', function *() { this.body = people; })); app.use(route.get('/people/:dni', function *(dni) { var person = people.find(function (p) { return p.dni == dni; }); if (!person) { return this.throw(404)

31. 32. 33. 34. 35. 36. 37. 38. 39. 40. 41. 42. 43. 44. 45. 46. 47. 48. 49. 50. 51. 52. 53. 54. 55. 56. 57. 58. 59. 60. 61.

} this.body = person; })); app.use(route.post('/people/:dni', function *(dni) { people.push(this.body); })); app.use(route.put('/people/:dni', function *(dni) { var newPerson = this.body; people = people.map(function (p) { if (p.dni == dni) { return newPerson; } return p; }); })); app.use(route.delete('/people/:dni', function *(dni) { var person = people.find(function (p) { return p.dni == dni; }); if (!person) { return this.throw(404) } people = people.filter(function (p) { return p.dni != dni; }); })); app.listen(3000);

Sails (http://sailsjs.org/) Se trata de un framework MVC modular que utiliza Express por debajo para gestionar la comunicación HTTP. Automatiza la creación de APIs de forma importante: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11.

$ sails new api_sails $ cd api_sails/ $ sails generate api user $ curl -v -X POST 127.0.0.1:1337/user -H "Content-Type: aplicati on/json" --data '{"dni": 1234, "name": "Jose", "age": 15}' * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0) > POST /user HTTP/1.1 > User-Agent: curl/7.37.1

12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26.

> Host: 127.0.0.1:1337 > Accept: * / * > Content-Type: application/json > Content-Length: 40 > * upload completely sent off: 40 out of 40 bytes < HTTP/1.1 200 OK < X-Powered-By: Sails < Access-Control-Allow-Origin: < Access-Control-Allow-Credentials: < Access-Control-Allow-Methods: < Access-Control-Allow-Headers: < Content-Type: application/json; charset=utf-8 < Content-Length: 145 < Set-Cookie: sails.sid=s%3AG7EbZ70cNKC23T2hnHLdcAGc.AXOtUdV0zM3 msk%2B0%2FfDOUX3Ip2WkOeyqoa2Ez8AlYUU; Path=/; HttpOnly < Date: Tue, 30 Sep 2014 00:52:31 GMT < Connection: keep-alive < { "dni": 1234, "name": "Jose", "age": 15, "createdAt": "2014-09-30T00:52:31.200Z", "updatedAt": "2014-09-30T00:52:31.200Z", "id": 4 }

27. 28. 29. 30. 31. 32. 33. 34. 35. 36. 37. 38. 39. $ curl -v -X POST 127.0.0.1:1337/user -H "Content-Type: aplicati on/json" --data '{"dni": 1235, "name": "Jose", "age": 25}' 40. 41. * Hostname was NOT found in DNS cache 42. * Trying 127.0.0.1... 43. * Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0) 44. > POST /user HTTP/1.1 45. > User-Agent: curl/7.37.1 46. > Host: 127.0.0.1:1337 47. > Accept: * / * 48. > Content-Type: application/json 49. > Content-Length: 40 50. > 51. * upload completely sent off: 40 out of 40 bytes 52. < HTTP/1.1 200 OK 53. < X-Powered-By: Sails 54. < Access-Control-Allow-Origin: 55. < Access-Control-Allow-Credentials: 56. < Access-Control-Allow-Methods: 57. < Access-Control-Allow-Headers: 58. < Content-Type: application/json; charset=utf-8 59. < Content-Length: 145 60. < Set-Cookie: sails.sid=s%3Az6-qkcaxuiDo_wgg_VloezIl.vqSaGAz8GJI l%2B8N%2BuRAhjyjz%2F1gqaxGQdsRiri%2BMUpM; Path=/; HttpOnly 61. < Date: Tue, 30 Sep 2014 00:53:31 GMT 62. < Connection: keep-alive

63. 64. 65. 66. 67. 68. 69. 70. 71. 72. 73. 74. 75. 76. 77. 78. 79. 80. 81. 82. 83. 84. 85. 86. 87. 88. 89. 90. 91. 92. 93. 94. 95. 96. 97. 98. 99. 100. 101. 102. 103. 104. 105. 106. 107. 108. 109. 110. 111. 112.

< { "dni": 1235, "name": "Jose", "age": 25, "createdAt": "2014-09-30T00:53:31.660Z", "updatedAt": "2014-09-30T00:53:31.660Z", "id": 5 } $ curl -v -XGET 127.0.0.1:1337/user * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0) > GET /user HTTP/1.1 > User-Agent: curl/7.37.1 > Host: 127.0.0.1:1337 > Accept: * / * > < HTTP/1.1 200 OK < X-Powered-By: Sails < Access-Control-Allow-Origin: < Access-Control-Allow-Credentials: < Access-Control-Allow-Methods: < Access-Control-Allow-Headers: < Content-Type: application/json; charset=utf-8 < Content-Length: 491 < Set-Cookie: sails.sid=s%3AqQiPi2H5UCelBVNzMIJqNHgj.FsAcdbrBsJH RIwyRNlYvEFH4INnjhWpznfjknBX4b3k; Path=/; HttpOnly < Date: Tue, 30 Sep 2014 00:53:51 GMT < Connection: keep-alive < [ { "dni": 1235, "name": "Jose", "age": 25, "createdAt": "2014-09-30T00:52:22.760Z", "updatedAt": "2014-09-30T00:52:22.761Z", "id": 3 }, { "dni": 1234, "name": "Jose", "age": 15, "createdAt": "2014-09-30T00:52:31.200Z", "updatedAt": "2014-09-30T00:52:31.200Z", "id": 4 } ]

Como puede observarse, la respuesta de creación de un recurso con Sails es un buen ejemplo de API que desobedece las directrices REST respecto a los códigos de estado de HTTP: en lugar de devolver un 201 (recurso creado) devuelve un 200 genérico de OK. No obstante, estas respuestas pueden modificarse con posterioridad. Sails genera una serie de archivos categorizados en 5 directorios: Responses: funciones mnemotécnicas para las respuestas HTTP de nuestra API. Controllers: funciones encargadas de atender las peticiones. Models: el modelo de datos. Services: utilidades al alcance de diferentes partes de la aplicación. Policies: autorización y control de acceso.

Hapi.js (http://hapijs.com/) Sigue un enfoque parecido al de Express, centrándose en facilitar la construcción de servicios web en lugar de preocuparse por la infraestructura de la aplicación. Tiene una forma propia de especificar rutas y manejadores: 1. var hapi = require('hapi'); 2. var server = new hapi.Server(3000); 3. 4. server.route({method: 'GET', path: '/people', handler: function (req, res) { 5. res(people); 6. }}); 7. 8. server.route({method: 'GET', path: '/people/{dni}', handler: fun ction (req, res) { 9. var person = people.filter(function (p) { 10. return p.dni == req.params.dni; 11. })[0]; 12. if (!person) { 13. return res(404); 14. } 15. res(person); 16. }}); 17. 18. ... 19. server.start();

Seguridad A la hora de desarrollar una API hay que tener en cuenta el control de acceso. En base a ello podemos tener tres tipos de APIs: API pública: Abierta a todo el mundo. No necesita ningún tipo de control de acceso. API privada: API protegida y de uso limitado a una serie de usuarios. API mixta: Algunos recursos son privados y otros públicos, o algunas acciones están permitidas y otras no sobre un mismo recurso para diferentes usuarios. Para controlar el acceso a un recurso es necesario identificar al usuario. Para ello existen varios mecanismos de autenticación: Basic authentication La más conocida y soportada por todo tipo de frameworks. Consiste en el envío de un login y password en la cabecera Authorization, codificados como username:password en Base 64. Por ejemplo: 1. HEADERS: { 2. "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", 3. ... 4. }

Para considerarlo un método seguro debe utilizarse en combinación con SSL, ya que transmitir credenciales en texto plano no es buena idea en otro caso. API key como parámetro de la URI o como header Consiste en añadir un paso adicional al control de acceso. En una primera fase el cliente envía los credenciales de usuario al servidor para obtener un token. A partir de ese momento el resto de peticiones incluyen dicho token de acceso. El servidor, por tanto, debe conocer la información de permisos asociada a cada token. 1. "http://esto_es_mi_api.com/people?auth=292j293j98fkl2jl1kxc v0"

Un posible problema es el robo de dicho token de acceso. Se pueden realizar algunas mejoras para evitarlo: Cambiar el token asociado al usuario cada vez que se realice una petición. El servidor puede enviar uno nuevo en la respuesta y desechar el token previo para evitar su futuro uso. De esta forma los tokens son de un sólo uso. No limitarse al token para controlar el acceso, sino comprobar también el User Agent, la IP u otra información del cliente. HMAC (Hash-based Message Authentication Code) Existe una clave secreta compartida entre cliente y servidor, y una clave privada que el cliente no tiene que compartir. Es utilizad por AWS y Azure, por ejemplo. OAuth 2.0: Se trata de la mejor alternativa. Es un método por token, pero la generación del mismo tiene lugar durante un proceso complejo.

Probando nuestra API Cuando desarrollamos APIs REST podemos realizar pruebas sencillas de funcionamiento, para analizar los headers y cuerpos de las respuestas de nuestro servicio, utilizando algo tan sencillo como CURL (http://es.wikipedia.org/wiki/CURL). 1. 2. 3. 4. 5. 6. 7. 8.

#!/bin/bash curl -X GET -v 127.0.0.1:3000/people curl -X GET -v 127.0.0.1:3000/people/111 curl -X POST -v -H "Content-Type: application/json" \ --data '{"dni": 444, "name": "Enrique", "age": 33}' 127.0.0. 1:3000/people/444

9. 10. curl -X PUT -v -H "Content-Type: application/json" \ 11. --data '{"dni": 444, "name": "Jose", "age": 33}' 127.0.0.1:3 000/people/444 12. 13. curl -X DELETE -v 127.0.0.1:3000/people/111

Por ejemplo, la petición GET sobre el recurso /people/333 daría como resultado: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20.

$ curl -X GET -v 127.0.0.1:3000/people/333 * Hostname was NOT found in DNS cache * Trying 127.0.0.1... * Connected to 127.0.0.1 (127.0.0.1) port 3000 (#0) > GET /people/333 HTTP/1.1 > User-Agent: curl/7.37.1 > Host: 127.0.0.1:3000 > Accept: * / * > < HTTP/1.1 200 OK < X-Powered-By: koa < Content-Type: application/json; charset=utf-8 < Content-Length: 36 < Date: Tue, 30 Sep 2014 00:18:00 GMT < Connection: keep-alive < * Connection #0 to host 127.0.0.1 left intact {"dni":333,"name":"Carlos","age":25}

Adicionalmente podemos hacer pruebas de carga sobre nuestro servidor para analizar su comportamiento en situaciones extremas. Por ejemplo, utilizando loadtest (https://www.npmjs.org/package/loadtest). En el siguiente ejemplo sometemos nuestra API a una prueba de carga, simulando 20 clientes que realizan 1000 peticiones GET /people por segundo.

1. $ loadtest -c 20 --rps 1000 http://127.0.0.1:3000/people 2. [Sep 30 2014 13:52:33] INFO Requests: 0, requests per second: 0, mean latency: 0 ms 3. [Sep 30 2014 13:52:38] INFO Requests: 4517, requests per second: 903, mean latency: 0 ms 4. [Sep 30 2014 13:52:43] INFO Requests: 9516, requests per second: 1000, mean latency: 0 ms 5. [Sep 30 2014 13:52:48] INFO Requests: 14520, requests per second : 1001, mean latency: 80 ms 6. [Sep 30 2014 13:52:53] INFO Requests: 17432, requests per second : 576, mean latency: 60 ms 7. [Sep 30 2014 13:52:58] INFO Requests: 21972, requests per second : 919, mean latency: 390 ms 8. [Sep 30 2014 13:52:58] INFO Errors: 3467, accumulated errors: 34 67, 15.8% of total requests 9. [Sep 30 2014 13:53:03] INFO Requests: 26972, requests per second : 1000, mean latency: 0 ms 10. [Sep 30 2014 13:53:03] INFO Errors: 5000, accumulated errors: 84 67, 31.4% of total requests

También es posible ver las peticiones por segundo que aguanta para un determinado número de clientes simultáneos: 1. $ loadtest -c 100 http://127.0.0.1:3000/people 2. [Sep 30 2014 14:10:07] INFO Requests: 0, requests per second: 0, mean latency: 0 ms 3. [Sep 30 2014 14:10:12] INFO Requests: 1872, requests per second: 374, mean latency: 80 ms 4. [Sep 30 2014 14:10:17] INFO Requests: 1872, requests per second: 0, mean latency: 0 ms 5. [Sep 30 2014 14:10:22] INFO Requests: 2965, requests per second: 216, mean latency: 580 ms 6. [Sep 30 2014 14:10:27] INFO Requests: 10552, requests per second : 1539, mean latency: 160 ms 7. [Sep 30 2014 14:10:32] INFO Requests: 13303, requests per second : 550, mean latency: 180 ms

Conclusiones Un servicio o API tiene que cumplir 3 requisitos fundamentales para ser considerado REST: 1. Uso correcto de URIs. 2. Uso correcto de HTTP. Métodos HTTP Códigos de estado

Formatos de contenido 3. Implementación de Hypermedia. La mayoría de APIs que dicen ser REST en realidad no lo son. Podemos probar cualquier API con CURL. El rendimiento es importante, debemos probar nuestras APIs en situaciones extremas. Node.js es una buena plataforma para la construcción de APIs REST. Existen diversos enfoques y módulos para construir APIs con Node.js.

Desarrollo de APIs RESTful con Node.js. - Sergio García Mondaray ([email protected] | @sgmonda)