node.js, Programación, Tutorial

Implementando un CRUD con node.js y MySQL

En este artículo vamos a ver como, usando node.js en conjunción con MySQL, podemos implementar un CRUD completo, y exponerlo como un API REST.

Express 4 es una infraestructura web pensada para la creación e implementación de APIs REST, principalmente con JSON como formato de intercambio de datos. Está basado en node.js, en un post anterior realizamos un pequeño ejemplo de Express 4, node.js y MySQL. En ese ejemplo, creamos una ruta base, que respondía con una pequeñísima estructura de datos en JSON. El principio de un API REST basado en JSON es ese, se «ataca» (término que se usa, no me preguntes por qué) la URL del servicio, y ese servicio responde con una estrucutra de datos en JSON.

REST es el acrónimo de «Representational State Transfer», que significa Transferencia de estado representacional. ¿Qué significa esto? Que en la comunicación con el servicio, la información intercambiada contiene toda la información requerida para la realización de la operación. Esto es, no existe un «estado» mantenido en el servidor para las entidades que se trabajan con esta capa de servicios. Para mas información sobre este paradigma, recomiendo consultar los siguientes vínculos:

Definiremos el modelo de negocio de nuestro API como el objeto de clase Gato. Vamos a implementar la funcionalidad CRUD de esta entidad usando Express. Para el almacenamiento de estos datos, vamos a utilizar MySQL.

Primero, tenemos que definir qué son estas entidades. Para simplificar, diremos que Gato tiene las siguientes características:

  • Nombre
  • Edad
  • Color
  • Raza
  • Peso

Primero, tendremos que crear nuestra base de datos MySQL. La llamaremos db_gatos, y la tabla en la que almacenaremos los datos de los gatos se lamará «gatos». La estructura de la tabla es como sigue:

  • id_gato (INT, PK, AUTONUM)
  • nombre (VARCHAR)
  • edad (INT)
  • peso (FLOAT)
  • raza (VARCHAR)
  • color (VARCHAR)

La tabla es muy sencilla, y no comtempla que, por ejemplo, las razas podrían estar en su propia tabla, para simplificar, y dado que lo que queremos es ver como funciona node.js con MySQL, usaremos esta estructura de base de datos. Una vez creada nuestra tabla en la base de datos de MySQL, ya podemos empezar a escribir el código de node.js para implementar las operaciones CRUD para la entidad gato. CRUD es el acrónimo de Create (Crear), Read (Leer), Update (Actualizar) y Delete (Borrar), y se considera el conjunto de operaciones básico para cualquier entidad.

En teoría, un API REST debería crearse correlacionando métodos HTTP con las operaciones del CRUD. En futuros APIs lo realizaremos de esa forma, pero en este caso, vamos a centrarnos en la interacción entre node.js, MySQL y Express. Por ello, nuestro API solo empleará los dos métodos HTTP mas usados: POST y GET. El mapeado de operaciones será como sigue:

  • Create: POST
  • Read: POST / GET
  • Update: POST
  • Delete: POST

Para cada una de las llamadas al API, tendremos que suministrar una estructura de datos del siguiente tipo:

{
  op: "CREATE/READ/UPDATE/DELETE",
  data_op: { field: data }
}

Siendo el campo op el que contiene la operación a realizar, y el campo data_op el que contiene la información (si procede), para la operación. Ahora, veamos qué tenemos que suministrar en cada una de las operaciones:

  • Create: Los datos que tenemos que pasar son: nombre, edad, peso, raza.
  • Read: Nada
  • Update: Los datos que tenemos que pasar son: id_gato, [campo(s)_a_actualizar]
  • Delete: El dato es id_gato

Las operaciones quedan definidas de esta forma, así que veamos el código fuente de node.js para la implementación de las mismas:

var express = require('express');
var bodyParser = require('body-parser'); 
var mysql = require('mysql');
var oApp = express(); 
oApp.use(bodyParser.json());
oApp.use(bodyParser.urlencoded({ extended: true })); 
var oMyConnection = mysql.createConnection({
	 host: '127.0.0.1',
	 user: 'root',
	 password: '1234',
     database: 'aalmunia_tests'	 
});

oApp.get('/gato', function(oReq, oRes) {
	 var sSQLGetAll = "SELECT * FROM gato";
	 oMyConnection.query(sSQLGetAll, function(oError, oRows, oCols) {
		 if(oError) {
			 oRes.write(JSON.stringify({
				 error: true,
				 error_object: oError				 
			 }));
			 oRes.end();
		 } else {
			 oRes.write(JSON.stringify(oRows));
			 oRes.end();			 
		 }
	 });
});
 
function CreateGATO(oDataGATO, oResponse) {	
	var sSQLCreate = "INSERT INTO gato (id_gato, nombre, raza, color, edad, peso, last_updated) VALUES (NULL, ";
	sSQLCreate += "'" + oDataGATO.nombre + "', ";
	sSQLCreate += "'" + oDataGATO.raza + "', ";
	sSQLCreate += "'" + oDataGATO.color + "', ";
	sSQLCreate += "'" + oDataGATO.edad + "', ";
	sSQLCreate += "'" + oDataGATO.peso + "', ";
	sSQLCreate += "NOW())";
		
	oMyConnection.query(sSQLCreate, function(oError, oRows, oCols) {
		if(oError) {
			oResponse.write(JSON.stringify({
				error: true,
				error_object: oError
			}));
			oResponse.end();			
		} else {
			var iIDCreated = oRows.insertId;
			oResponse.write(JSON.stringify({
				error: false,
				idCreated: iIDCreated
			}));
			oResponse.end();			
		}		
	});
} 
 
function ReadGATO(oResponse) {
	var sSQLRead = "SELECT * FROM gato";
	oMyConnection.query(sSQLRead, function(oError, oRows, oCols) {
		if(oError) {
			oResponse.write(JSON.stringify({
				error: true,
				error_object: oError
			}));
			oResponse.end();
		} else {
			oResponse.write(JSON.stringify({
				error: false,
				data: oRows
			}));
			oResponse.end();						
		}		
	});		
}

function UpdateGATO(oDataGATO, oResponse) {
	var sSQLUpdate = "UPDATE gato SET last_updated = NOW() ";
	if(oDataGATO.hasOwnProperty('nombre')) {
		sSQLUpdate += " AND nombre = '" + oDataGATO.nombre + "' ";
	}
	if(oDataGATO.hasOwnProperty('raza')) {
		sSQLUpdate += " AND raza = '" + oDataGATO.raza + "' ";
	}
	if(oDataGATO.hasOwnProperty('color')) {
		sSQLUpdate += " AND color = '" + oDataGATO.color + "' ";
	}
	if(oDataGATO.hasOwnProperty('edad')) {
		sSQLUpdate += " AND edad = '" + oDataGATO.edad + "' ";
	}
	if(oDataGATO.hasOwnProperty('peso')) {
		sSQLUpdate += " AND peso = '" + oDataGATO.peso + "' ";		
	}		
	sSQLUpdate = " WHERE idgato = '" + oDataGATO.idgato + "'";
	
	oMyConnection.query(sSQLUpdate, function(oErrUpdate, oRowsUpdate, oColsUpdate) {
		if(oErrUpdate) {
			oResponse.write(JSON.stringify({ 
				error: true,
				error_object: oErrUpdate
			}));
			oResponse.end();			
		} else {
			oResponse.write(JSON.stringify({
				error: false
			}));
			oResponse.end();
		}
	});
}

function DeleteGATO(oDataGATO, oResponse) {
	var sSQLDelete = "DELETE FROM gato WHERE idgato = '" + oDataGATO.idgato + "'";
	oMyConnection.query(sSQLDelete, function(oErrDelete, oRowsDelete, oColsDelete) {
		if(oErrDelete) {
			oResponse.write(JSON.stringify({
				error: true,
				error_object: oErrDelete
			}));
			oResponse.end();
		} else {
			oResponse.write(JSON.stringify({
				error: false
			}));
			oResponse.end();			
		}		
	});	
}
 
 oApp.post('/gato', function(oReq, oRes) {
	 var oDataOP = {};
	 var sOP = '';
	 
	 oDataOP = oReq.body.data_op;
	 sOP = oReq.body.op;
	 
	 switch(sOP) {
		 
		 case 'CREATE':			
			CreateGATO(oDataOP, oRes);
		 break;
		 
		 case 'READ':
			ReadGATO(oRes);
		 break;
		 
		 case 'UPDATE':
			UpdateGATO(oDataOP, oRes);
		 break;
		 
		 case 'DELETE':
			DeleteGATO(oDataOP, oRes);
		 break;
		 
		 default:
			oRes.write(JSON.stringify({ 
				error: true, 
				error_message: 'Debes proveer una operación a realizar' 
			}));
			oRes.end();
		 break;
		 
	 }	 
 });
 
 oApp.listen(9016, function(oReq, oRes) {
	 console.log("Servicios web gestión entidad GATO activo, en puerto 9016");	 
 });

Vamos a realizar un análisis del código fuente que hemos escrito. Lo primero que tenemos es lo siguiente:

var express = require('express');
var bodyParser = require('body-parser'); 
var mysql = require('mysql');
var oApp = express();
 
oApp.use(bodyParser.json());
oApp.use(bodyParser.urlencoded({ extended: true }));
 
var oMyConnection = mysql.createConnection({
	 host: '127.0.0.1',
	 user: 'root',
	 password: '1234',
     database: 'aalmunia_tests'	 
});

Antes de probar el código, tenemos que estar seguros de que tenemos las dependencias de la aplicación en el sistema.
Para ello, usaremos el gestor de paquetes de node, llamado npm. Para poder instalar los módulos, ejecutaremos
la siguiente instrucción en la carpeta de la aplicación:

npm install express body-parser mysql

Tras eso, debería haber creado una carpeta llamada node_modules, en la que están ubicados los módulos. Una vez
que tenemos las librerías, analicemos un poco el código fuente que tenemos.

require es una instrucción que sirve para importar módulos de node.js. Al importarlos, se crea un
objeto global a la aplicación que contiene el módulo. Algunos módulos se pueden usar directamente tras su
importación, y otros tenemos que crear un objeto de la librería (una instancia, en terminología OOP). Los módulos que
usamos para la aplicación son Express 4, body-parser, y la librería de acceso a MySQL desde node, node-mysql.

Las instrucciones siguientes:

oApp.use(bodyParser.json());
oApp.use(bodyParser.urlencoded({ extended: true }));

le están indicando a la aplicación de Express que los parámetros de las peticiones pueden venir en un formato JSON, o como campos de un formulario. Es muy importante, de lo contrario vas a pasar mucho tiempo preguntándote por qué no ve el cuerpo de las peticiones, con los parámetros correspondientes.

Tras eso, instanciamos una conexión a la base de datos de MySQL. Este objeto conexión será el que usemos para realizar cualquier consulta contra la base de datos.

En este punto, es interesante tener en cuenta que estamos usando MySQL sin una capa por encima que suele ser un ORM. Esto es así para que veamos como funciona realizar consultas con la librería de MySQL, sobre todo para ver la asincronicidad que hay que tener en cuenta para casi todo en node.js. Veamos ahora la parte que inicializa nuestra aplicación:

 oApp.listen(9016, function(oReq, oRes) {
	 console.log("Servicios web gestión entidad GATO activo, en puerto 9016");	 
 });

Solo con esa sencilla instrucción, estamos poniendo el objeto oApp, que es nuestra aplicación de express, a escuchar peticiones HTTP en el puerto 9016. Como norma general, se suele escoger un puerto alto, para no interferir con el resto de servicios y daemons que podrían estar siendo ejecutados en la máquina en la que corre nuestro servidor de node.js. Por si sola, esta instrucción no hace nada. Si conectamos a http://127.0.0.1:9016/, no vamos a ver nada, solo una página en blanco. Para que nuestro servidor haga algo mas allá de escuchar simplemente en un puerto, debemos empezar a definir rutas. Las rutas se definen por las siguientes características:

  • Método HTTP (GET, POST, PUT, OPTIONS, DELETE, etc…)
  • URL de la ruta
  • Parámetros que acepta
  • Salida que ofrece

Antes de programar ninguna línea de código, deberíamos siempre intentar completar esta tabla para nuestro API REST. Puede (casi siempre es así), que en un futuro se realicen cambios sobre la especificación original, pero lo importante es que esta fase de modelado del API no debe ser desestimada. Así pues, rellenemos nuestra tabla:

HTTP URL OPERACIÓN Parámetros Salida
GET /gato LECTURA Ninguno
[
  {"id_gato": "XXX",
   "nombre": "XXX",
   "edad", "XXX",
   "color": "XXX",
   "raza": "XXX",
   "peso": "XXX"},
  {"id_gato": "XXX",
   "nombre": "XXX",
   "edad", "XXX",
   "color": "XXX",
   "raza": "XXX",
   "peso": "XXX"},
  { ... }
]
POST /gato LECTURA
        {"op": "READ"}
Ídem que en GET /gato
POST /gato CREACIÓN
        {
         "op": "CREATE",
         "data_op": {
           "name": "XXX",
           "edad": "XXX",
           "color": "XXX",
           "raza": "XXX",
           "peso": "XXX"         
         }
        }
        {
         "op_result": "OK/KO", 
         "id_gato": "XXX"
        }      
POST /gato MODIFICACIÓN
        {
         "op": "UPDATE",
         "data_op": {
           "id_gato": "XXX",
           "campo(s)_a_actualizar": "XXX"
         }
        }
        {"op_result": "OK/KO"}
POST /gato BORRADO
        {
         "op": "DELETE",
         "data_op": {
           "id_gato": "XXX" 
         }
       }      
        {"op_result": "OK/KO"}

De esta forma, queda bastante claro qué tenemos que enviar a cada servicio, y qué es lo que esperamos recibir al realizar una llamada. Esta tabla es muy importante para cualquiera que necesite implementar un cliente que consuma nuestro API. Dicho de otra forma, con esta tabla, un desarrollador que necesite nuestro API, no tiene por qué esperar a que lo terminemos. Puede realizar sus propios mock-ups de servicios, de tal forma que le devolverán exactamente lo mismo que nuestro servicio. Al trabajar de esta forma, cualquier cambio en la especificación del API debe ser comunicado a aquellos que implementan un cliente del API.

Veamos ahora como esta tabla se traslada a código fuente de node.js. Lo primero, es definir las rutas GET /gato y POST /gato. Veamos la implementación de GET /gato:

oApp.get('/gato', function(oReq, oRes) {
	 var sSQLGetAll = "SELECT * FROM gato";
	 oMyConnection.query(sSQLGetAll, function(oError, oRows, oCols) {
		 if(oError) {
			 oRes.write(JSON.stringify({
				 error: true,
				 error_object: oError				 
			 }));
			 oRes.end();
		 } else {
			 oRes.write(JSON.stringify(oRows));
			 oRes.end();			 
		 }
	 });
});

Lo primero, si llamamos a [OBJETO_EXPRESS].get(), estamos definiendo una ruta de método HTTP GET. Los parámentros que acepta esta función son dos: La URL de la ruta, que debe ser pasada en modo de cadena, y una función de callback, que recibe dos argumentos: el objeto de petición y el objeto de respuesta. Con la respuesta contestamos al cliente que haya accedido a nuestra ruta. Dentro del callback, tenemos la llamada a la base de datos MySQL, que se realiza llamando al método .query() del objeto oMyConnection que hemos definido al principio de nuestro programa. Esta función recibe dos argumentos, la consulta SQL a ejecutar, y una función de callback que recibe tres potenciales argumentos: Un error, si se produce, las filas obtenidas en la consulta, y las columnas obtenidas en la consulta. Recomiendo leer mas sobre la librería de node/mysql, para poder entender los callbacks que la misma espera. La sentencia SQL que ejecutamos contra la base de datos es SELECT * FROM gato. De ese modo, obtendremos toda la tabla. Al ser esto un tutorial, el uso de SELECT * se realiza por comodidad, pero no esperes que en un entorno profesional de trabajo no te llamen la atención por utilizar esa sentencia SQL. Es muy trabajoso, si la tabla tiene muchos campos y registros, extraer toda esa información.

En el código vemos como lo primero que revisamos es si el objeto oError viene con algún valor. Si no se produjo ningún error, el valor del objeto oError es null, por tanto no se cumple esa condición. Respondemos pues, usando el método .write(), con las filas. Para terminar de escribir en el buffer de salida, y enviarlo al cliente, usamos el método .end().

Y ya está. El resto del código es simple de entender con estas explicaciones. La ruta de POST tiene un switch para ver qué operación se ha enviado al API. El resto de operaciones son muy similares a como hemos llamado a la base de datos en la ruta GET /gato. No debería presentar ninguna dificultad si hasta ahora has entendido el código que hemos ido viendo.

Y ahora es cuando llegan las malas noticias: Nuestro API es inseguro. Al no haber realizado ninguna comprobación de los parámetros de entrada, estamos expuestos a un ataque de tipo inyección de SQL, en el que un cliente malicioso puede enviar SQL para ser ejecutado en nuestro API, obteniendo en la salida, en vez de una lista de gatos, una lista de las contraseñas de toda la instancia del servidor MySQL en el que esté alojado nuestro API. ¿Qué hacer? En el próximo tutorial veremos como realizar estas cosas «a mano», y también como utilizar un ORM para abstraernos del SQL, y poder modelar objetos de negocio, de tal forma que ya no tengamos que preocuparnos de la sintaxis exacta de una complejísima instrucción SQL.

Leave a Comment

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *