Primeros pasos para crear un API REST con Node.js
Si vas a crear un API REST, Node.js es una gran opción, es una tecnología que está muy de actualidad, empresas y herramientas importantes están apostando por esta tecnología. Además Node.js tiene una forma diferente de gestionar las peticiones, en otras tecnologías cada petición se realiza en un hilo distinto y cada hilo consume memoria con lo que hay un limite de peticiones simultáneas dependiendo de la memoria disponible en servidor. En Nodejs todas las peticiones son manejadas por un solo hilo pero de forma asíncrona, delegando la realización de las tareas en un pool de threads, de esta forma Nodejs tiene una mayor capacidad de soportar peticiones simultáneas. Otra ventaja sería la posibilidad de compartir librerías propias entre cliente y servidor al utilizar un lenguaje común, JavaScript.
Introducción a Restify
Restify es un módulo de Node.js específico para crear API REST. Posiblemente el más utilizado es Express pero es más genérico, por lo tanto le faltan cosas para desarrollar un API REST y tiene cosas que no se utilizan en este caso.
Creando el API REST
Lo primero que tenemos que tener es un fichero package.json, va a contener información relacionada al proyecto. Para empezar es suficiente con el nombre de proyecto, versión y dependencias. En esta sección de dependencias añadimos Restify.
{
"name": "Catalog-API",
"version": "0.0.1",
"dependencies": {
"restify": "4.0.3"
}
}
Es necesario que nos posicionemos, utilizando el terminal, en la carpeta del proyecto y ejecutar el comando npm install. Este comando descargará mediante el gestor de paquetes el módulo Restify y lo incluirá dentro de una carpeta node_modules.
También necesitamos un fichero app.js como punto de entrada. Este fichero será el que ejecutaremos con el motor de Node.js.
"use strict"
var restify = require('restify');
var server = restify.createServer();
server.get('/api/products/', (req,res,next) =>
{
res.json([{sku: 'ab48cicj36734', asin: 'B015E8UTIU', upc: '888462500449',
title: 'Apple iPhone 6s 64 GB US Warranty Unlocked Cellphone - Retail Packaging (Rose Gold)',
image: 'http://ecx.images-amazon.com/images/I/91DpCeCgSBL._SL1500_.jpg' }]);
next();
});
server.listen(8080, function() {
console.log('%s listening at %s', server.name, server.url);
});
Invocando el API REST
Si desde el terminal ejecutamos node app.js, ya lo tenemos listo para recibir peticiones.
Si realizamos una petición Get:
GET /api/products HTTP/1.1
Host: localhost:8080
Cache-Control: no-cache
Obtenemos como respuesta el json que estamos devolviendo:
[
{
"sku": "ab48cicj36734",
"asin": "B015E8UTIU",
"upc": "888462500449",
"title": "Apple iPhone 6s 64 GB US Warranty Unlocked Cellphone - Retail Packaging (Rose Gold)",
"image": "http://ecx.images-amazon.com/images/I/91DpCeCgSBL._SL1500_.jpg"
}
]
Con esto tenemos lo mínimo necesario para tener un API REST que tiene un endpoint para devolver productos, pero es habitual que en el mundo real tengamos más endpoints y tengamos algo bastante más grande, así que es conveniente empezar a estructurar nuestra capa de presentación.
Introducción el patrón MVC
El patrón Modelo Vista Controlador no es un tipo de arquitectura, es un patrón para la capa de presentación. Esta capa en una arquitectura de capas es solo una de toda la arquitectura.
El patrón Modelo Vista Controlador es un patrón de presentación bastante utilizado. Sirve para separar responsabilidades de forma que por cada endpoint vamos a tener:
Modelo: puerta de enlace a la lógica de negocio o capa de dominio que van a proporcionar los los datos y la lógica de negocio de la aplicación. Provee al controlador el acceso a la información a devolver en la vista.
Según el tipo de aplicación y la arquitectura que estés utilizando puede significar una cosa u otra. Por ejemplo si estas utilizando Clean Arquitecture propuesto por Robert C. Martín, el modelo sería un Interactor o UseCase. Si estas utilizando la arquitectura de capas propuesta por Eric Evans en el libro Domain-Driven Design: Tackling Complexity in the Heart of Software, el modelo sería un Application Service. Y si tu aplicación no tiene apenas lógica de negocio y es un simple CRUD, el modelo podría ser un ActiveRecord.
Vista: elemento con el que interactúa el usuario, en nuestro caso al ser una api el usuario va a ser otro componente de software y la vista va a ser los datos que devolvemos, en este caso un json. En otros escenarios como una web la vista sería el html devuelto.
Controlador: hace de intermediario entre el modelo y la vista. Se encarga de pedir datos al modelo para devolvérselos a la vista y también de realizar acciones en el modelo cuyo origen se produce en la vista.
Aplicando el patrón MVC
Para empezar por lo más básico sería crearnos una carpeta controllers y crearnos un fichero productController.js. Este controlador se va a encargar de manejar todas las peticiones al endpoint /api/products
.
'use strict'
module.exports = class productController {
constructor() {
this.products = new Array();
}
get(req, res) {
res.send(200,this.products);
}
post(req, res) {
var product = {
sku: req.body.sku,
asin: req.body.asin,
upc: req.body.upc,
title: req.body.title,
image: req.body.image
};
this.products.push(product);
res.send(201,res.header('Location', '/api/products/' + product.sku));
};
}
Es bastante simple pero suficiente para entender la idea.
También necesitamos un mecanismo de registrar las rutas de los endpoints a los métodos de cada controlador. En algunas tecnologías se utiliza convención sobre configuración como en Web API de .Net, donde si existe un método get en un controlador ProductController, se asume que ese método maneja la ruta GET /products. En otras tecnologías como Spring de Java o la propia Web Api de .net también se pueden decorar los métodos del controlador con atributos o anotaciones y es el propio framework el encargado de registrar la rutas e invocar los métodos del controlador.
En restify no existen mecanismos de registro de rutas mediante convención o decoración de métodos, tenemos que hacerlo manualmente. Si la API es pequeña puede ser una opción válida, si es grande puede ser momento de buscar una solución más automática.
Necesitamos una carpeta routes y creamos un fichero productRoutes.js.
"use strict"
var productController = require('../controllers/productController.js');
module.exports = class Routes {
static register(server) {
const controller = new productController();
server.get('/api/products/', (req,res,next) =>
{
controller.get(req,res);
next();
});
server.post('/api/products/', (req,res,next) =>
{
controller.post(req,res);
next();
});
}
}
Para este ejemplo no vamos a tener base de datos de momento, por eso me he declarado el controlador como const controller = new knowronController();
, para que entre peticiones se mantenga la lista de productos del controlador. En un ejemplo más real por cada petición se debe de crear un controlador nuevo y no mantener el estado entre peticiones.
Finalmente modificamos el fichero de entrada app.js para dinámicamente hacer el require de los ficheros routes e invocamos el método register pasando el servidor de restify.
"use strict"
var restify = require('restify');
var fs = require('fs');
var server = restify.createServer();
server.use(restify.acceptParser(server.acceptable));
server.use(restify.queryParser());
server.use(restify.bodyParser());
fs.readdirSync('./routes').forEach(function(curFile) {
if (curFile.substr(-3) === '.js') {
let routes = require('./routes/' + curFile);
routes.register(server);
}
});
server.listen(8080, function() {
console.log('%s listening at %s', server.name, server.url);
});
Mediante server.use
añadimos middlewares que se va a encargar de acciones como parsear el body en las peticiones post etc..
Faltaría el modelo, donde accederíamos a una base de datos y a la capa de negocio o dominio, pero esta parte la dejo para otro post, porque es un tema que me gustaría extenderme y este artículo ya se queda un poco largo.
Invocando el API REST
Volvemos a realizar peticiones sobre el api rest. Ahora si realizamos una petición Get:
GET /api/products HTTP/1.1
Host: localhost:8080
Cache-Control: no-cache
No devuelve nada.
[]
Si realizamos una petición Post:
POST /api/products HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Cache-Control: no-cache
{"sku": "ab48cicj36734", "asin": "B015E8UTIU", "upc": "888462500449","title": "Apple iPhone 6s 64 GB US Warranty Unlocked Cellphone - Retail Packaging (Rose Gold)","image": "http://ecx.images-amazon.com/images/I/91DpCeCgSBL._SL1500_.jpg"}
Y realizamos de nuevo una petición Get:
GET /api/products HTTP/1.1
Host: localhost:8080
Cache-Control: no-cache
Nos devuelve un array con el elemento creado.
[
{
"sku": "ab48cicj36734",
"asin": "B015E8UTIU",
"upc": "888462500449",
"title": "Apple iPhone 6s 64 GB US Warranty Unlocked Cellphone - Retail Packaging (Rose Gold)",
"image": "http://ecx.images-amazon.com/images/I/91DpCeCgSBL._SL1500_.jpg"
}
]
Conclusiones
Node.js es una tecnología muy potente y que se esta abriendo hueco. Su mayor potencial es el manejo de la concurrencia, pero además la utilización de un lenguaje común en servidor y cliente. Hemos visto que es una buena opción para crear un API REST y las ventajas que tiene.
Todo el código utilizado está subido a GitHub, os animo a descargarlo, probarlo.
Recursos
Como con cualquier tecnología es importante seguir buenas prácticas y tener una buena arquitectura en una api.
En esta página de servicios, encontrarás información sobre formación que imparto sobre buenas prácticas.