Fundamentos de Nodejs implementando métodos de persistencia

La persistencia de datos es una de las características más importantes de un API Rest ya que esto nos permitirá almacenar los datos que enviemos a nuestro API. Los frameworks como Express nos ayudan mucho con la persistencia, ya que traen funciones especiales para guardar información o conectarse a una base de datos de manera directa sin hacer verificaciones previas de la petición. Pero esta es la continuación del tutorial anterior de Fundamentos de Nodejs donde comenzamos desarrollando un API Rest desde cero sin Frameworks. Así que la persistencia que implementaremos se basará en guardar los datos en archivos de texto. Pero para lograr esto tenemos que implementar nuestras propias funciones para guardar, leer, actualizar y eliminar archivos.

Esta es la segunda parte de la serie Fundamentos de Nodejs donde aprenderemos a implementar métodos de persistencia en archivos de texto con el objetivo de continuar el proyecto de un API REST usando solo Nodejs sin Frameworks.

Índice

  1. Manejo de archivos en Nodejs
  2. Creando el método para escribir archivos
  3. Creando el método para leer archivos
  4. Creando el método para actualizar un archivo
  5. Creando un método para eliminar un archivo
  6. El código

1. Manejo de archivos en Nodejs

Para poder manejar archivos en Nodejs existe el modulo File System fs el cual debemos traer a nuestro proyecto de la siguiente forma.

const fs = require("fs");

La operación más sencilla que podemos realizar con File System es la de leer un archivo utilizando el método readFile, pero antes debemos crear un archivo de texto, por ejemplo data.txt con cualquier contenido de texto en su interior. Luego utilizamos el siguiente código para leer este archivo en Nodejs.

// Traemos el modulo necesario
const fs = require("fs");

// Uitlizamos el método readFile para leer un archivo
fs.readFile("data.txt", (err, data) => {
  if (err) {
    console.log("Error al leer el archivo");
  } else {
    // Mostramos el contenido del archivo
    console.log(data);
  }
});

Sin embargo el resultado de este código será en formato hexadecimal. Esto es porque interna mente en este método Nodejs utiliza una clase Buffer que sirve para manejar datos binarios que, al fin y al cabo, es lo que son todos los archivos 1s y 0s. Pero para evitarnos la molestia de colocar tantos números en pantalla Buffer convierte el resultado en hexadecimal, como vemos a continuación.

<Buffer 48 6f 6c 61 20 41 6e 64 79>

Por esta razón el método readFile tiene un parámetro opcional donde indicamos la codificación que queremos que tenga el resultado. Así que utilizamos la codificación utf-8. Esta codificación agrupa los binarios en 8bits y los convierte en caracteres ASCII que son las letras y números con los que escribo este post. Así que para leer el contenido correctamente utilizamos el siguiente código.

const fs = require("fs");

fs.readFile("data.txt", "utf-8", (err, data) => {
  if (err) {
    console.log("Error al leer el archivo");
  } else {
    console.log(data);
  }
});

Sin embargo, tanto el método readFile() como writeFile() son abreviaciones que nos evitan usar el método open() pero solo se utilizan cuando realizaremos operaciones muy simples con el archivo. Pero si queremos realizar operaciones más complejas como crear un archivo para colocar contenido traído de múltiples fuentes o modificar un archivo debemos usar el método open().

fs.open(filename, mode, callback);

La forma más básica de utilizar este método es usando esos tres parámetros. El primer parámetro filename indica la ruta del archivo a abrir. El segundo parámetro mode indica bajo que condiciones queremos abrir el archivo. Por ejemplo si colocamos wx crea un archivo nuevo para escribirlo pero falla si la ruta que le enviamos ya existe. En otras palabras son condiciones que debe tener la ruta para que funcione bien y de esta manera saber cual fue el error y capturarlo. Los demás modos están en el siguiente enlace. Por último el callback es el método que se ejecuta después de abrir el archivo. Este debe contener dos parámetros erry data. El primero almacena el error, si es que lo hay y el segundo almacena el contenido del archivo que abrimos.

2. Creando el método para escribir archivos

Ahora comenzaremos implementando los métodos para nuestro API REST que tenemos pendiente del tutorial anterior. Así que pueden comenzar de la siguiente rama del repositorio. Comenzaremos con el método de crear archivos de texto para almacenar los datos que nos enviarán mediante el método POST a un endpoint de usuarios que implementaremos en el próximo tutorial.

Cada uno de estos métodos los escribiremos en un archivo llamado data.js dentro de una nueva carpeta llamada lib. Entonces dentro de este archivo comenzaremos con el método create con el siguiente código que paso a explicar a continuación.

/* Traemos los modulos necesarios para el manejo de archivos (fs)
 * y para trabajar con rutas de directorios tenemos el modulo path
 */
var fs = require("fs");
var path = require("path");

/* Creamos el contenedor para los métodos que crearemos (create, update,...)
 * Este contenedor será un objeto con métodos en su interior
 * la idea es exportar este objeto para utilizarlo en el archivo
 * de nuestro endpoint que escuchará el método POST
 */
var lib = {};

/* Utilizando el modulo path creamos la ruta para nuestro directorio donde
 * guardaremos los archivos con los datos (la carpeta data de nuestro proyecto)
 * __dirname se utiliza para obtener la ruta donde se ubica el archivo que codificamos
 * en este caso, donde está ubicado data.js
 * path.join solo une la ruta actual(__dirname) con "../../data" creando una nueva ruta
 */
lib.baseDir = path.join(__dirname, "./../data/");

/* Aquí creamos nuestra función lib.create que recibirá 4 parámetros
 * dir para enviarle la ruta donde se creará el archivo
 * file para indicarle el nombre del archivo
 * data para indicarle los datos que se guardarán en el archivo
 * callback para indicarle el método que se ejecutará despues de terminal la función
 * este callback solamente mostrará un mensaje mediante console log
 * indicando el estado de la creación o si hubo algun error
 */
lib.create = function (dir, file, data, callback) {
  /* Utilizamos el método fs.open para abrir un archivo
   * enviamos como parámetro wx para obtener un error cuando ya exista un archivo igual
   */
  fs.open(
    lib.baseDir + dir + "/" + file + ".json",
    "wx",
    function (err, fileDescriptor) {
      // Validamos que no haya error y que el contenido sea valido
      if (!err && fileDescriptor) {
        // Convirte datos a string
        var stringData = JSON.stringify(data);
        // Utilizamos fs.writeFile para escirbir el string anterior en el archivo
        fs.writeFile(fileDescriptor, stringData, function (err) {
          // Validamos que no haya error
          if (!err) {
            // Si no hay error cerramos el archivo
            fs.close(fileDescriptor, function (err) {
              if (!err) {
                // Si todo salio bien el callback será false
                callback(false);
              } else {
                // Si hay error se muestra mediante el callback
                callback("Error al cerrar el archivo");
              }
            });
          } else {
            callback("Error al abrir el archivo");
          }
        });
      } else {
        callback("No se pudo crear el archivo " + err);
      }
    }
  );
};

// Exportamos el modulo lib, para usarlo en otros archivos
module.exports = lib;

Ahora probaremos nuestro método de creación de archivos para esto en el archivo app.js utilizamos el siguiente código.

// Importamos la librería que creamos
var _data = require("./lib/data");

// Utilizamos nuestra libreria creando un archivo (newFile) en la carpeta test
_data.create("test", "newFile", { foo: "bar" }, function (err) {
  console.log("Este fue el error", err);
});

Para que nuestro método funcione correctamente debemos crear la carpeta data y dentro la carpeta test. La primera porque es la ruta por defecto que programamos para que busque usando lib.baseDir y la carpeta test porque le enviamos ese nombre como parámetro a nuestro método create, que como sabemos une las rutas para ubicar nuestro archivo final.

Al ejecutar nuestro código por primera vez el archivo se creara pero si lo ejecutamos otra vez ya no pasara eso porque el archivo ya existe y el parámetro que pusimos wx en el método fs.open() evita que se vuelva a crear un archivo si este ya existe. Pero listo!! ya tenemos nuestro primer método para la librería que estamos creando que nos permitirá crear archivos.

3. Creando el método para leer archivos

Ahora el segundo método que crearemos para nuestra librería es el que nos servirá para leer archivos. Este método lo usaremos cuando hagamos un request GET a nuestro endpoint users que implementaremos en el próximo tutorial. Nuestro API REST buscará el archivo del usuario lo leerá y lo enviará de vuelta al cliente.

Así que manos a la obra y comencemos nuevamente dentro de nuestro archivo data.js creando la función lib.read. Esta función es más simple que la anterior y como se trata de una operación simple de lectura, que devuelve lo que está escrito en el archivo basta con usar fs.readFile(). Así que aquí no usaremos fs.open().

lib.read = function (dir, file, callback) {
  fs.readFile(
    lib.baseDir + dir + "/" + file + ".json",
    "utf-8",
    function (err, data) {
      // En este caso el callback se encargará de llevar el error y la respuesta
      callback(err, data);
    }
  );
};

Para probar este método dentro de nuestro archivo app.js recordemos que el callback es el que trae los parámetros de err y data así que los mostramos con un console.log de la siguiente manera.

// Intentaremos leer el archivo (newFile) que creamos con el método create
_data.read("test", "newFile", function (err, data) {
  console.log("Este fue el error", err, "y estos son los datos", data);
});

4. Creando el método para actualizar un archivo

Ahora implementaremos el método que nos permitirá modificar los datos de nuestro archivo. Este método lo utilizaremos cuando el cliente envíe una petición para actualizar un usuario en nuestro endpoint users. Entonces crearemos el método llamado lib.update con el siguiente código que explicaré a continuación.

// Funcion para actualizar datos de un archivo
lib.update = function (dir, file, data, callback) {
  /* Utilizamos fs.open para abrir el archivo
   * y usamos el modo r+ para abrir, leer y escribir en el archivo
   */
  fs.open(
    lib.baseDir + dir + "/" + file + ".json",
    "r+",
    function (err, fileDescriptor) {
      // Validamos que no haya errores
      if (!err && fileDescriptor) {
        var stringData = JSON.stringify(data);

        // Truncar el archivo significa eliminar el contenido sin necesidad de borrar el archivo
        fs.ftruncate(fileDescriptor, function (err) {
          if (!err) {
            // Luego de eliminar el contenido lo volvemos a llenar con la nueva data
            fs.writeFile(fileDescriptor, stringData, function (err) {
              if (!err) {
                fs.close(fileDescriptor, function (err) {
                  if (!err) {
                    /* Al igual que en el método del POST el callback lo utilizaremos para
                     * mostrar un mensaje en la consola
                     */
                    callback(false);
                  } else {
                    callback("Error cerrando el archivo");
                  }
                });
              } else {
                callback("Error escribiendo el archivo existente");
              }
            });
          } else {
            callback("Error truncando el archivo");
          }
        });
      } else {
        callback("No se pudo abrir el archivo quizas no exista");
      }
    }
  );
};

Si queremos probar nuestro método solo bastará con utilizar el siguiente código dentro de nuestro archivo app.js.

_data.update("test", "newFile", { name: "andygeek" }, function (err) {
  console.log("Este fue el error", err);
});

5. Creando un método para eliminar un archivo

Para terminar nuestra librería necesitamos implementar un método que nos permita eliminar archivos. Esto para cuando desde el cliente nos envíen una petición DELETE para nuestros usuarios. Así que crearemos el método lib.delete donde utilizaremos el siguiente código.

lib.delete = function (dir, file, callback) {
  // El siguiente método sirve para eliminar el archivo con la ruta que le estamos enviando
  fs.unlink(lib.baseDir + dir + "/" + file + ".json", function (err) {
    if (!err) {
      callback(false);
    } else {
      callback("Error eliminando el archivo");
    }
  });
};

Este método será muy fácil de probar solo necesitamos un archivo con el nombre newFile y utilizar el método que implementamos en el archivo app.js o cualquier parte de nuestro proyecto.

_data.delete("test", "newFile", function (err) {
  console.log("Este fue el error", err);
});

Luego de probar los métodos con node app.js no te olvides de comentar el código de las pruebas ya que no las utilizaremos más.

Y listo!! ya tenemos nuestra librería data que nos ayudará con la persistencia de datos en nuestro proyecto. En el próximo tutorial implementaremos el endpoint users que nos permitirá agregar datos, los cuales se guardará utilizando estos método dentro de archivos en nuestro proyecto.

6. El código

El código de este proyecto está en el siguiente repositorio github.com/andygeek/server-only-nodejs. Pero te sugiero intentarlo tu mismo antes de ver el código.