Cómo implementar almacenamiento en caché en Node.js usando Redis

El autor seleccionó /dev/color para recibir una donación como parte del programa Write for DOnations.

Introducción

La mayoría de las aplicaciones dependen de datos, ya sea de una base de datos o de una API. Obtener datos de una API implica enviar una solicitud de red al servidor de la API y recibir los datos como respuesta. Estos viajes de ida y vuelta llevan tiempo y pueden aumentar el tiempo de respuesta de tu aplicación a los usuarios. Además, la mayoría de las APIs limitan la cantidad de solicitudes que pueden atender a una aplicación dentro de un marco de tiempo específico, un proceso conocido como limitación de tasa.

Para solucionar estos problemas, puedes cachear tus datos para que la aplicación realice una única solicitud a una API, y todas las solicitudes de datos posteriores recuperarán los datos de la caché. Redis, una base de datos en memoria que almacena datos en la memoria del servidor, es una herramienta popular para cachear datos. Puedes conectarte a Redis en Node.js utilizando el módulo node-redis, que te proporciona métodos para recuperar y almacenar datos en Redis.

En este tutorial, construirás una aplicación Express que recupera datos de una API RESTful utilizando el módulo axios. A continuación, modificarás la aplicación para almacenar los datos obtenidos de la API en Redis utilizando el módulo node-redis. Después, implementarás el período de validez de la caché para que esta expire después de cierto tiempo transcurrido. Finalmente, utilizarás el middleware de Express para almacenar en caché los datos.

Requisitos previos

Para seguir el tutorial, necesitarás:

Paso 1 — Configuración del Proyecto

En este paso, instalarás las dependencias necesarias para este proyecto y comenzarás un servidor Express. En este tutorial, crearás una wiki que contiene información sobre diferentes tipos de peces. Llamaremos al proyecto fish_wiki.

Primero, crea el directorio para el proyecto usando el comando mkdir:

  1. mkdir fish_wiki

Ingresa al directorio:

  1. cd fish_wiki

Inicializa el archivo package.json utilizando el comando npm:

  1. npm init -y

La opción -y acepta automáticamente todos los valores predeterminados.

Cuando ejecutas el comando npm init, creará el archivo package.json en tu directorio con el siguiente contenido:

Output
Wrote to /home/your_username/<^>fish_wiki<^/package.json: { "name": "fish_wiki", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

A continuación, instalarás los siguientes paquetes:

  • express: un marco de servidor web para Node.js.
  • axios: un cliente HTTP de Node.js, útil para realizar llamadas a API.
  • node-redis: un cliente de Redis que te permite almacenar y acceder a datos en Redis.

Para instalar los tres paquetes juntos, introduce el siguiente comando:

  1. npm install express axios redis

Después de instalar los paquetes, crearás un servidor Express básico.

Usando nano o el editor de texto de tu elección, crea y abre el archivo server.js:

  1. nano server.js

En tu archivo server.js, introduce el siguiente código para crear un servidor Express:

fish_wiki/server.js
const express = require("express");

const app = express();
const port = process.env.PORT || 3000;


app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Primero, importas express al archivo. En la segunda línea, defines la variable app como una instancia de express, lo que te da acceso a métodos como get, post, listen y muchos más. Este tutorial se enfocará en los métodos get y listen.

En la línea siguiente, defines y asignas la variable port al número de puerto en el que deseas que el servidor escuche. Si no hay disponible ningún número de puerto en un archivo de variables ambientales, se utilizará el puerto 3000 como predeterminado.

Finalmente, utilizando la variable app, invoca el método listen() del módulo express para iniciar el servidor en el puerto 3000.

Guarda y cierra el archivo.

Ejecuta el archivo server.js usando el comando node para iniciar el servidor:

  1. node server.js

La consola mostrará un mensaje similar al siguiente:

Output
App listening on port 3000

La salida confirma que el servidor está en funcionamiento y listo para atender cualquier solicitud en el puerto 3000. Como Node.js no recarga automáticamente el servidor cuando se modifican los archivos, ahora detendrás el servidor usando CTRL+C para poder actualizar server.js en el próximo paso.

Una vez que hayas instalado las dependencias y creado un servidor Express, recuperarás datos de una API RESTful.

Paso 2 — Recuperar Datos de una API RESTful Sin Caché

En este paso, construirás sobre el servidor Express del paso anterior para recuperar datos de una API RESTful sin implementar caché, demostrando qué sucede cuando los datos no se almacenan en caché.

Para empezar, abre el archivo server.js en tu editor de texto:

  1. nano server.js

A continuación, recuperarás datos de la API FishWatch. La API FishWatch devuelve información sobre especies de peces.

En tu archivo server.js, define una función que solicite datos de la API con el siguiente código resaltado:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

En la segunda línea, importas el módulo axios. A continuación, defines una función asíncrona fetchApiData(), que toma species como parámetro. Para hacer que la función sea asíncrona, la prefijas con la palabra clave async.

Dentro de la función, llamas al método get() del módulo axios con el punto final de la API de la que deseas que el método recupere los datos, que es la API FishWatch en este ejemplo. Dado que el método get() implementa una promesa, la prefijas con la palabra clave await para resolver la promesa. Una vez que la promesa se resuelve y se devuelven los datos de la API, llamas al método console.log(). El método console.log() registrará un mensaje diciendo que se ha enviado una solicitud a la API. Finalmente, devuelves los datos de la API.

A continuación, definirás una ruta Express que acepte solicitudes GET. En tu archivo server.js, define la ruta con el siguiente código:

fish_wiki/server.js
...

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  ...
});

En el bloque de código anterior, invocas el método get() del módulo express, que solo escucha las solicitudes GET. El método toma dos argumentos:

  • /fish/:species: el punto final en el que Express estará escuchando. El punto final toma un parámetro de ruta :species que captura cualquier cosa ingresada en esa posición en la URL.
  • getSpeciesData() (aún no definido): una función de devolución de llamada que se llamará cuando la URL coincida con el punto final especificado en el primer argumento.

Ahora que la ruta está definida, especifique la función de devolución de llamada getSpeciesData:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
}
app.get("/fish/:species", getSpeciesData);
...

La función getSpeciesData es una función de controlador asíncrona pasada al método get() del módulo express como segundo argumento. La función getSpeciesData() toma dos argumentos: un objeto de solicitud y un objeto de respuesta. El objeto de solicitud contiene información sobre el cliente, mientras que el objeto de respuesta contiene la información que se enviará al cliente desde Express.

A continuación, agregue el código resaltado para llamar a fetchApiData() para recuperar datos de una API en la función de devolución de llamada getSpeciesData():

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  results = await fetchApiData(species);
}
...

En la función, extraes el valor capturado del punto final almacenado en el objeto req.params, luego lo asignas a la variable species. En la siguiente línea, defines la variable results y la estableces en undefined.

Después de eso, invoca la función fetchApiData() con la variable species como argumento. La llamada a la función fetchApiData() está precedida por la sintaxis await porque devuelve una promesa. Cuando la promesa se resuelve, devuelve los datos, que luego se asignan a la variable results.

A continuación, agrega el código resaltado para manejar errores en tiempo de ejecución:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

Defines el bloque try/catch para manejar errores en tiempo de ejecución. En el bloque try, llamas a fetchApiData() para recuperar datos de una API. Si se produce un error, el bloque catch registra el error y devuelve un código de estado 404 con una respuesta de “Datos no disponibles”.

La mayoría de las APIs devuelven un código de estado 404 cuando no tienen datos para una consulta específica, lo que activa automáticamente el bloque catch para ejecutarse. Sin embargo, la API de FishWatch devuelve un código de estado 200 con un array vacío cuando no hay datos para esa consulta específica. Un código de estado 200 significa que la solicitud fue exitosa, por lo que el bloque catch() nunca se activa.

Para activar el bloque catch(), debes verificar si el array está vacío y lanzar un error cuando la condición if se evalúa como verdadera. Cuando las condiciones if se evalúan como falsas, puedes enviar una respuesta al cliente que contenga los datos.

Para hacer eso, agrega el código resaltado:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  ...
  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

Una vez que los datos son devueltos desde la API, la declaración if verifica si la variable results está vacía. Si se cumple la condición, se utiliza la declaración throw para lanzar un error personalizado con el mensaje La API devolvió un array vacío. Después de ejecutarse, la ejecución cambia al bloque catch, que registra el mensaje de error y devuelve una respuesta 404.

Por el contrario, si la variable results contiene datos, la condición de la declaración if no se cumplirá. Como resultado, el programa omitirá el bloque if y ejecutará el método send del objeto de respuesta, que envía una respuesta al cliente.

El método send toma un objeto que tiene las siguientes propiedades:

  • fromCache: la propiedad acepta un valor que te ayuda a saber si los datos provienen de la caché de Redis o de la API. Ahora asignaste un valor false porque los datos provienen de una API.

  • data: la propiedad se asigna con la variable results que contiene los datos devueltos desde la API.

En este punto, tu código completo se verá así:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");

const app = express();
const port = process.env.PORT || 3000;

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Ahora que todo está en su lugar, guarda y sale del archivo.

Inicia el servidor express:

  1. node server.js

La API Fishwatch acepta muchas especies, pero solo utilizaremos la especie de pez red-snapper como parámetro de ruta en el endpoint que estarás probando a lo largo de este tutorial.

Ahora abre tu navegador web favorito en tu ordenador local. Navega a la URL http://localhost:3000/fish/red-snapper.

Nota: Si estás siguiendo el tutorial en un servidor remoto, puedes ver la aplicación en tu navegador utilizando el reenvío de puerto.

Con el servidor Node.js aún en funcionamiento, abre otra terminal en tu ordenador local y luego ingresa el siguiente comando:

  1. ssh -L 3000:localhost:3000 your-non-root-user@yourserver-ip

Una vez conectado al servidor, navega a http://localhost:3000/fish/red-snapper en el navegador web de tu máquina local.

Una vez que se cargue la página, deberías ver fromCache establecido en false.

Ahora, actualiza la URL tres veces más y observa tu terminal. La terminal registrará “Solicitud enviada a la API” tantas veces como hayas actualizado tu navegador.

Si actualizaste la URL tres veces después de la visita inicial, tu salida se verá así:

Output
App listening on port 3000 Request sent to the API Request sent to the API Request sent to the API Request sent to the API

Esta salida muestra que se envía una solicitud de red al servidor de la API cada vez que actualizas el navegador. Si tuvieras una aplicación con 1000 usuarios golpeando el mismo endpoint, eso serían 1000 solicitudes de red enviadas a la API.

Cuando implementas el almacenamiento en caché, la solicitud a la API solo se hará una vez. Todas las solicitudes posteriores obtendrán datos de la caché, mejorando el rendimiento de tu aplicación.

Por ahora, detén tu servidor Express con CTRL+C.

Ahora que puedes solicitar datos de una API y servirlos a los usuarios, almacenarás datos devueltos de una API en Redis.

Paso 3 — Almacenamiento en caché de solicitudes de API RESTful usando Redis

En esta sección, almacenarás datos de la API para que solo la visita inicial al punto final de tu aplicación solicite datos a un servidor de API, y todas las solicitudes siguientes recuperarán datos de la caché de Redis.

Abre el archivo server.js:

  1. nano server.js

En tu archivo server.js, importa el módulo node-redis:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");
...

En el mismo archivo, conecta a Redis usando el módulo node-redis agregando el código resaltado:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  ...
}
...

Primero, defines la variable redisClient con el valor establecido en undefined. Después de eso, defines una función asíncrona anónima autoinvocada, que es una función que se ejecuta inmediatamente después de definirla. Definir una función asíncrona anónima autoinvocada consiste en encerrar una definición de función sin nombre entre paréntesis (async () => {...}). Para hacer que se autoinvoque, la sigues inmediatamente con otro conjunto de paréntesis (), lo que termina luciendo así (async () => {...})().

Dentro de la función, invocas el método createClient() del módulo redis que crea un objeto redis. Dado que no proporcionaste el puerto para que Redis lo utilice al invocar el método createClient(), Redis usará el puerto 6379, el puerto predeterminado.

También llamas al método on() de Node.js que registra eventos en el objeto Redis. El método on() toma dos argumentos: error y un callback. El primer argumento error es un evento que se desencadena cuando Redis encuentra un error. El segundo argumento es un callback que se ejecuta cuando se emite el evento error. El callback registra el error en la consola.

Finalmente, llamas al método connect(), que inicia la conexión con Redis en el puerto predeterminado 6379. El método connect() devuelve una promesa, por lo que la precedes con la sintaxis await para resolverla.

Ahora que tu aplicación está conectada a Redis, modificarás el callback getSpeciesData() para almacenar datos en Redis en la visita inicial y recuperar los datos de la caché para todas las solicitudes que siguen.

En tu archivo server.js, agrega y actualiza el código resaltado:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
     }
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

En la función getSpeciesData, defines la variable isCached con el valor false. Dentro del bloque try, llamas al método get() del módulo node-redis con species como argumento. Cuando el método encuentra la clave en Redis que coincide con el valor de la variable species, devuelve los datos, los cuales luego se asignan a la variable cacheResults.

A continuación, una declaración if verifica si la variable cacheResults tiene datos. Si se cumple la condición, se asigna a la variable isCached el valor true. Después de esto, invocas el método parse() del objeto JSON con cacheResults como argumento. El método parse() convierte datos de cadena JSON en un objeto JavaScript. Después de que el JSON ha sido analizado, invocas el método send(), que toma un objeto que tiene la propiedad fromCache establecida en la variable isCached. El método envía la respuesta al cliente.

Si el método get() del módulo node-redis no encuentra datos en la caché, la variable cacheResults se establece en null. Como resultado, la evaluación de la declaración if da como resultado falso. Cuando eso sucede, la ejecución salta al bloque else donde llamas a la función fetchApiData() para obtener datos de la API. Sin embargo, una vez que los datos son devueltos por la API, no se guardan en Redis.

Para almacenar los datos en la caché de Redis, necesitas usar el método set() del módulo node-redis para guardarlo. Para hacer eso, agrega la línea resaltada:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    ...
  }
}
...

Dentro del bloque else, una vez que se ha obtenido los datos, llamas al método set() del módulo node-redis para guardar los datos en Redis bajo el nombre de clave del valor en la variable species.

El método set() toma dos argumentos, que son pares clave-valor: species y JSON.stringify(results).

El primer argumento, species, es la clave bajo la cual se guardarán los datos en Redis. Recuerda que species está establecido con el valor pasado al punto final que definiste. Por ejemplo, cuando visitas /fish/red-snapper, species se establece en red-snapper, que será la clave en Redis.

El segundo argumento, JSON.stringify(results), es el valor para la clave. En el segundo argumento, invocas el método stringify() de JSON con la variable results como argumento, que contiene datos devueltos por la API. El método convierte JSON en una cadena; por eso, cuando recuperaste datos de la caché usando el método get() del módulo node-redis anteriormente, invocaste el método JSON.parse con la variable cacheResults como argumento.

El archivo completo se verá así:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results));
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Guarda y cierra tu archivo, y ejecuta server.js usando el comando node:

  1. node server.js

Una vez que el servidor haya iniciado, actualiza http://localhost:3000/fish/red-snapper en tu navegador.

Observa que fromCache aún está establecido en false:

Ahora actualiza la página de nuevo para ver que esta vez fromCache está establecido en true:

Actualiza la página cinco veces y regresa al terminal. Tu salida se verá similar a lo siguiente:

Output
App listening on port 3000 Request sent to the API

Ahora, Request sent to the API solo se ha registrado una vez después de múltiples actualizaciones de URL, contrastando con la última sección donde el mensaje se registraba en cada actualización. Esta salida confirma que solo se envió una solicitud al servidor y que posteriormente, los datos se obtienen de Redis.

Para confirmar aún más que los datos están almacenados en Redis, detén tu servidor usando CTRL+C. Conéctate al cliente del servidor Redis con el siguiente comando:

  1. redis-cli

Recupera los datos bajo la clave red-snapper:

  1. get red-snapper

Tu salida será similar a lo siguiente (editada por brevedad):

Output
"[{\"Fishery Management\":\"<ul>\\n<li><a...3\"}]"

La salida muestra la versión stringify de los datos JSON que la API devuelve cuando visitas el punto final /fish/red-snapper, lo cual confirma que los datos de la API están almacenados en la caché de Redis.

Sal del cliente del servidor Redis:

  1. exit

Ahora que puedes almacenar en caché datos de una API, también puedes establecer la validez de la caché.

Paso 4 — Implementando la Validez de la Caché

Al almacenar datos en caché, es necesario saber con qué frecuencia cambian los datos. Algunos datos de API cambian en minutos; otros en horas, semanas, meses o años. Establecer una duración de caché adecuada garantiza que su aplicación sirva datos actualizados a sus usuarios.

En este paso, establecerá la validez de la caché para los datos de la API que deben almacenarse en Redis. Cuando la caché expire, su aplicación enviará una solicitud a la API para recuperar datos recientes.

Necesita consultar la documentación de su API para establecer el tiempo de vencimiento correcto para la caché. La mayoría de la documentación mencionará con qué frecuencia se actualizan los datos. Sin embargo, hay algunos casos en los que la documentación no proporciona la información, por lo que es posible que deba hacer conjeturas. Verificar la propiedad last_updated de varios puntos finales de la API puede mostrar con qué frecuencia se actualizan los datos.

Una vez que elija la duración de la caché, debe convertirla a segundos. Para fines de demostración en este tutorial, establecerá la duración de la caché en 3 minutos o 180 segundos. Esta duración de muestra facilitará la prueba de la funcionalidad de duración de la caché.

Para implementar la duración de validez de la caché, abra el archivo server.js:

  1. nano server.js

Agregue el código resaltado:

fish_wiki/server.js
const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  ...
})();

async function fetchApiData(species) {
  ...
}

async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

En el método set() del módulo node-redis, pasa un tercer argumento que es un objeto con las siguientes propiedades:

  • EX: acepta un valor con la duración de la caché en segundos.
  • NX: cuando se establece en true, asegura que el método set() solo debe establecer una clave que aún no exista en Redis.

Guarde y salga del archivo.

Vuelve al cliente del servidor Redis para probar la validez del caché:

  1. redis-cli

Elimina la clave red-snapper en Redis:

  1. del red-snapper

Sal del cliente Redis:

  1. exit

Ahora, inicia el servidor de desarrollo con el comando node:

  1. node server.js

Vuelve a tu navegador y actualiza la URL http://localhost:3000/fish/red-snapper. Durante los próximos tres minutos, si actualizas la URL, la salida en la terminal debería ser consistente con la siguiente salida:

Output
App listening on port 3000 Request sent to the API

Después de que hayan pasado tres minutos, actualiza la URL en tu navegador. En la terminal, deberías ver que se ha registrado “Solicitud enviada a la API” dos veces:

Output
App listening on port 3000 Request sent to the API Request sent to the API

Esta salida muestra que el caché ha caducado y se realizó nuevamente una solicitud a la API:

Puedes detener el servidor de Express:

Ahora que puedes establecer la validez del caché, a continuación, guardarás datos en caché usando middleware:

Paso 5 — Almacenamiento en caché de datos en Middleware

En este paso, usarás el middleware de Express para almacenar en caché datos. El middleware es una función que puede acceder al objeto de solicitud, al objeto de respuesta y a una devolución de llamada que debe ejecutarse después de que se ejecute. La función que se ejecuta después del middleware también tiene acceso al objeto de solicitud y de respuesta. Al usar middleware, puedes modificar los objetos de solicitud y de respuesta o devolver una respuesta al usuario antes.

Para usar middleware en tu aplicación para el almacenamiento en caché, modificarás la función del controlador getSpeciesData() para obtener datos de una API y almacenarlos en Redis. Moverás todo el código que busca datos en Redis a la función de middleware cacheData.

Cuando visites el endpoint /fish/:species, la función de middleware se ejecutará primero para buscar datos en la caché; si los encuentra, devolverá una respuesta y la función getSpeciesData no se ejecutará. Sin embargo, si el middleware no encuentra los datos en la caché, llamará a la función getSpeciesData para obtener datos de la API y almacenarlos en Redis.

Primero, abre tu server.js:

  1. nano server.js

A continuación, elimina el código resaltado:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;
  let isCached = false;

  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      isCached = true;
      results = JSON.parse(cacheResults);
    } else {
      results = await fetchApiData(species);
      if (results.length === 0) {
        throw "API returned an empty array";
      }
      await redisClient.set(species, JSON.stringify(results), {
        EX: 180,
        NX: true,
      });
    }

    res.send({
      fromCache: isCached,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

En la función getSpeciesData(), elimina todo el código que busca datos almacenados en Redis. También eliminas la variable isCached ya que la función getSpeciesData() solo obtendrá datos de la API y los almacenará en Redis.

Una vez que se haya eliminado el código, establece fromCache en false como se muestra a continuación, para que la función getSpeciesData() se vea de la siguiente manera:

fish_wiki/server.js
...
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}
...

La función getSpeciesData() obtiene los datos de la API, los almacena en la caché y devuelve una respuesta al usuario.

A continuación, agrega el siguiente código para definir la función de middleware para almacenar en caché los datos en Redis:

fish_wiki/server.js
...
async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}

async function getSpeciesData(req, res) {
...
}
...

La función middleware cacheData() toma tres argumentos: req, res y next. En el bloque try, la función verifica si el valor en la variable species tiene datos almacenados en Redis bajo su clave. Si los datos están en Redis, se devuelven y se establecen en la variable cacheResults.

Luego, la declaración if verifica si cacheResults tiene datos. Los datos se guardan en la variable results si la evaluación es verdadera. Después de eso, el middleware utiliza el método send() para devolver un objeto con las propiedades fromCache establecida en true y data establecida en la variable results.

Sin embargo, si la declaración if se evalúa como falsa, la ejecución cambia al bloque else. Dentro del bloque else, se llama a next(), que pasa el control a la siguiente función que debe ejecutarse después de ella.

Para hacer que el middleware cacheData() pase el control a la función getSpeciesData() cuando se invoque next(), actualice el método get() del módulo express de la siguiente manera:

fish_wiki/server.js
...
app.get("/fish/:species", cacheData, getSpeciesData);
...

El método get() ahora toma cacheData como su segundo argumento, que es el middleware que busca datos almacenados en Redis y devuelve una respuesta cuando se encuentran.

Ahora, cuando visitas el endpoint /fish/:species, cacheData() se ejecuta primero. Si los datos están en caché, devolverá la respuesta y el ciclo de solicitud-respuesta termina aquí. Sin embargo, si no se encuentran datos en la caché, se llamará a getSpeciesData() para recuperar datos de la API, almacenarlos en la caché y devolver una respuesta.

El archivo completo se verá así ahora:

fish_wiki/server.js

const express = require("express");
const axios = require("axios");
const redis = require("redis");

const app = express();
const port = process.env.PORT || 3000;

let redisClient;

(async () => {
  redisClient = redis.createClient();

  redisClient.on("error", (error) => console.error(`Error : ${error}`));

  await redisClient.connect();
})();

async function fetchApiData(species) {
  const apiResponse = await axios.get(
    `https://www.fishwatch.gov/api/species/${species}`
  );
  console.log("Request sent to the API");
  return apiResponse.data;
}

async function cacheData(req, res, next) {
  const species = req.params.species;
  let results;
  try {
    const cacheResults = await redisClient.get(species);
    if (cacheResults) {
      results = JSON.parse(cacheResults);
      res.send({
        fromCache: true,
        data: results,
      });
    } else {
      next();
    }
  } catch (error) {
    console.error(error);
    res.status(404);
  }
}
async function getSpeciesData(req, res) {
  const species = req.params.species;
  let results;

  try {
    results = await fetchApiData(species);
    if (results.length === 0) {
      throw "API returned an empty array";
    }
    await redisClient.set(species, JSON.stringify(results), {
      EX: 180,
      NX: true,
    });

    res.send({
      fromCache: false,
      data: results,
    });
  } catch (error) {
    console.error(error);
    res.status(404).send("Data unavailable");
  }
}

app.get("/fish/:species", cacheData, getSpeciesData);

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

Guarda y sal de tu archivo.

Para probar el almacenamiento en caché correctamente, puedes eliminar la clave red-snapper en Redis. Para hacerlo, entra en el cliente de Redis:

  1. redis-cli

Elimina la clave red-snapper:

  1. del red-snapper

Sal del cliente de Redis:

  1. exit

Ahora, ejecuta el archivo server.js:

  1. node server.js

Una vez que el servidor inicie, vuelve al navegador y visita nuevamente http://localhost:3000/fish/red-snapper. Actualízalo varias veces.

El terminal registrará el mensaje de que se envió una solicitud a la API. El middleware cacheData() servirá todas las solicitudes durante los próximos tres minutos. Tu salida se verá similar a esto si actualizas aleatoriamente la URL en un lapso de cuatro minutos:

Output
App listening on port 3000 Request sent to the API Request sent to the API

El comportamiento es consistente con cómo funcionaba la aplicación en la sección anterior.

Ahora puedes almacenar datos en caché en Redis usando middleware.

Conclusión

En este artículo, construiste una aplicación que obtiene datos de una API y devuelve los datos como respuesta al cliente. Luego modificaste la aplicación para almacenar en caché la respuesta de la API en Redis en la visita inicial y servir los datos desde la caché para todas las solicitudes posteriores. Modificaste la duración de esa caché para que expire después de cierto tiempo transcurrido, y luego utilizaste middleware para manejar la recuperación de datos de la caché.

Como próximo paso, puedes explorar la documentación de Node Redis para aprender más sobre las características disponibles en el módulo node-redis. También puedes leer la documentación de Axios y Express para obtener una visión más profunda sobre los temas cubiertos en este tutorial.

Para continuar desarrollando tus habilidades en Node.js, consulta la serie Cómo Codificar en Node.js.

Source:
https://www.digitalocean.com/community/tutorials/how-to-implement-caching-in-node-js-using-redis