How to Cache Node.js Applications with Redis

How to Cache Node.js Applications with Redis
Hostman Team
Technical writer
Redis
26.12.2024
Reading time: 11 min

Caching is the process of storing copies of files in a cache — a temporary storage that is much faster to access than other available storage methods in the system.

When developing Node.js applications, caching becomes highly relevant because database queries can take significantly longer than fetching data from temporary storage.

For example, there is no need to reload the HTML markup of a webpage for every user request to the server — this would add several (sometimes dozens of) milliseconds to the response time. Storing the page (or JSON data for rendering in a SPA application) is much more efficient in the cache.

In simple terms, caching is about optimization.

This article will explore how to cache application data in a Node.js application using Redis with the Express framework.

What is Redis?

Redis (Remote Dictionary Server) is an open-source, in-memory database with simple "key-value" data structures.

The terminology may vary. Some refer to Redis as a database, others as a caching tool, or something else. The key point is that Redis stores data in RAM instead of a hard drive, which results in higher performance. This is why Redis is referred to as an "in-memory" database.

Although the data is kept in RAM, it is periodically saved to a hard drive in the form of snapshots.

Redis is often used together with relational DBMSs, such as managed PostgreSQL.

Installing Redis Server

The installation process for Redis differs depending on the operating system, and you can find detailed instructions for each system on the official website.

This article focuses on Ubuntu or Debian. Therefore, we will install the latest version of Redis from the official APT (Advanced Packaging Tool) repository — packages.redis.io:

sudo apt update
sudo apt install redis

Once this is done, the Redis server is ready to use. 

For Windows, you need to download the installer from the official GitHub repository. After installation, start the Redis server with the following CLI command:

redis-cli

For macOS, you can install Redis using the Homebrew package manager:

brew install redis

Once installed, start the server with:

redis-server

Node.js Project Configuration

Before we dive into how to interact with Redis through a Node.js application, let's first create a separate working directory and navigate to it:

mkdir node_redis
cd node_redis

As usual, let's create a package.json configuration file with a minimal set of data:

{
 "name": "node_redis",
  "version": "1.0.0",
  "description": "Simple example of using Redis by Hostman",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
     "express": "latest",
     "axios": "latest",
     "redis": "latest"
  }
}

Note the specified dependencies. For this project, we will need the latest versions of the Express framework and the official Redis client for Node.js from NPM. This is a separate library that provides a high-level API (classes and functions) for interacting with a Redis server.

The Axios module will help parse the JSON data the remote server will return in response to API requests.

To install these dependencies, we will use the NPM package manager. If you don't have it yet, install it with the following command:

sudo apt install npm

You can read a separate guide on how to install the latest version of Node.js on Ubuntu. Since the app will use the async/await syntax, the minimum required version of Node.js is 8.

Now, once all dependencies are specified, they can be installed:

npm install

Express Application Without Caching

In this example, the application will use a fake API from JSONPlaceholder, specifically created for such purposes. We will send a request to the URL https://jsonplaceholder.typicode.com/posts/1 and receive mock data in JSON format:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}

Subsequent loading of data from the cache (instead of making repeated requests to the remote server) will increase the speed of the application. However, we will first implement the process of handling user requests without caching and add it later.

Let's first create and edit our index.js file. The script will use modern JavaScript (ES6) syntax with async/await operators whenever possible:

const express = require("express"); // import the Express framework
const axios = require("axios"); // import the Axios module for working with JSON data

const app = express(); // create an instance of the app

// create an async function to request data from the remote server using axios
async function getRemoteData() {
  const information = await axios.get(`https://jsonplaceholder.typicode.com/posts/1`); // send a request to the remote API
  console.log("There was a request to a remote server"); // log the informational message to the console
  return information.data; // return the raw JSON data
}

// create an async function to handle user requests
async function onRequest(req, res) {
  let results = await getRemoteData(); // call the previously created function to get data from the remote server

  if(results.length === 0) throw "API error"; // handle empty responses with an error

  res.send(results); // respond to the user's request with the raw JSON data
}

app.get('/', onRequest); // attach the previously created function to the GET request hook
app.listen(8080); // start listening for incoming requests on the default HTTP server port

Now, you can run the script, open localhost in your browser, and see the raw JSON data displayed on the web page:

node index.js

Each request to the local server will, in turn, trigger a request to the remote server. For example, if you refresh the page three times in the browser, the message "There was a request to a remote server" will be printed three times in the terminal of the running Node.js server.

But why? From a rational perspective, this is unnecessary. The data retrieved the first time should be cached to reduce the number of operations and user wait times. This is relevant only when the data is expected to remain static for a certain period — in other words, you can only cache data that doesn't change frequently.

Express Application with Caching

Let's modify the previous example so our application "learns" to cache data.

To do this, we'll first connect the Redis client — add a new line at the beginning of the index.js:

const redis = require("redis"); 

Now, naturally, we need to connect to the Redis server we started earlier, and only after that can we set and get keys. Let's add a few more lines of code:

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

  client.on("error", (error) => console.log('Something went wrong', error)); // set up an error handler for Redis connection issues

  await client.connect(); // connect to the Redis server
})();

Note that the connection to the Redis server is done in an anonymous self-invoking asynchronous function. This ensures that all pre-configuration steps are executed sequentially. Additionally, the connect function returns a promise, which can be handled using then/catch or inside an async function.

In our example, the caching logic will be as follows: if the API request to the remote server is made for the first time, we cache the obtained data. If the data has been previously retrieved, it will be available in the cache — we fetch it and send it to the user.

Let's modify the onRequest function (middleware) to implement caching:

async function onRequest(req, res) {
  let results; // declare the variable for the result

  const cacheData = await client.get("post"); // try to get the "post" key from Redis database

  if (cacheData) {
      results = JSON.parse(cacheData); // parse the data from a raw string format into a structure
  } else {
      results = await getRemoteData(); // call the function to get data from the remote server
      if (results.length === 0) throw "API error"; // handle empty result with an error
      await client.set("post", JSON.stringify(results)); // cache the obtained data
  }

  res.send(results); // respond to the request with JSON data
}

Notice that the get function returns null if no value is saved for the given key in Redis. If this happens, an API request is made to the remote server. If data exists in the cache, it is retrieved and sent to the user.

The set function is responsible for caching — it stores the given value under a specified key so we can retrieve it later with get.

The full code of the application at this stage looks like this:

const express = require("express"); // import Express framework
const axios = require("axios"); // import Axios module for working with JSON data
const redis = require("redis"); // import Redis client

const app = express(); // create an instance of the application

// Connect to Redis server
(async () => {
  client = redis.createClient();

  client.on("error", (error) => console.log('Something went wrong', error)); // set up an error handler for Redis connection issues

  await client.connect(); // connect to the Redis server
})();

// create an asynchronous function to request data from the remote server using axios
async function getRemoteData() {
  const information = await axios.get(`https://jsonplaceholder.typicode.com/posts/1`); // send a request to the remote server with the API
  console.log("There was a request to a remote server"); // log an informational message to the console
  return information.data; // return the obtained JSON data in raw form
}

// create an asynchronous function to handle user requests
async function onRequest(req, res) {
  let results; // declare the variable for the result

  const cacheData = await client.get("post"); // attempt to retrieve the "post" variable from the Redis database

  if (cacheData) {
      results = JSON.parse(cacheData); // parse the data from a raw string into a structured format
  } else {
      results = await getRemoteData(); // call the function to fetch data from the remote server
      if (results.length === 0) throw "API error"; // handle empty result with an error
      await client.set("post", JSON.stringify(results)); // cache the obtained data
  }

  res.send(results); // respond with the JSON data
}

// run the HTTP server with the necessary configurations
app.get('/', onRequest); // associate the created function with the GET request hook
app.listen(8080); // start handling incoming requests on the standard HTTP server port

Setting Cache Expiration

We should periodically update the data stored in the cache to prevent it from becoming outdated.

In real-world projects, APIs often provide additional information about how frequently cached data should be updated. This information is used to set a timeout — the duration for which the data in the cache remains valid. Once this time expires, the application makes a new request to obtain fresh data.

In our case, we will take a simpler approach that is commonly used in practice. We will set a constant cache expiration time of 60 seconds. After this period, the application will make another request to the remote server for fresh data.

It’s important to note that cache expiration is handled by Redis. This can be achieved by providing additional parameters when using the set function.

To implement this, we will modify the set function call to include an additional structure as the third argument. Thus, the line:

await client.set("post", JSON.stringify(results)); // cache the received data

Will be updated to:

await client.set("post", JSON.stringify(results), { EX: 60, NX: true }); // cache the received data with expiration

In this case, we updated the previous line of code by adding the EX parameter, which is the cache expiration time in seconds. The NX parameter ensures that the key is only set if it does not already exist in the Redis database. This last parameter is important because re-setting the key would update the cache timeout without it, preventing it from fully expiring.

Now, the Redis database will store the value of the post key for 60 seconds and then delete it. This means that every minute, the cacheData variable in our app will receive a null value, triggering an API request to the remote server and re-caching the obtained result.

Conclusion

This article demonstrated how in-memory storage can serve as a "mediator" between processing and storing data on solid-state drives.

All of this is a form of caching that reduces unnecessary computational (and network) operations, thereby improving the application's performance and reducing the server's load.

As shown, you can quickly set up such storage using Redis with a Node.js client. In our case, we used a mock API that returned trivial JSON data. In one scenario, the data was requested every time, while in the other, it was cached — sometimes with an expiration time.

The examples provided are just the basics. As always, you can find more information on using Redis in the official documentation. The same applies to the documentation for Express and Axios.

Redis
26.12.2024
Reading time: 11 min

Similar

Redis

How to Manage Redis Keys and Databases

Redis is a NoSQL, open-source database management system that stores data in memory as key-value pairs, where each key is a unique identifier for its associated values. A single Redis instance can host multiple databases, each capable of storing various data types. Advantages of Redis High processing speed: It can handle up to 110,000 SET and 81,000 GET operations per second. Support for advanced data types: Besides strings, Redis databases can include lists, sets (including sorted sets), and hashes. Atomicity: Operations are atomic, so each client receives consistent data regardless of the number of simultaneous requests. Versatility: Redis is effective for caching, message queuing, and storing short-term information (e.g., during web sessions). Ease of setup and use: Redis’s simplicity makes it popular for development with Python, PHP, Java, and more. In this article, we’ll cover some basic commands for managing keys and databases in Redis. Each example is independent and does not need to be executed in sequence, so they can be reviewed individually. We’ll execute all commands on a server running Ubuntu 22.04 with Redis version 7.0.12, using the redis-cli utility. However, these commands are compatible with other interfaces (such as Redli) and with cloud-based Redis databases. Redis Data Types Before diving into commands, let's look at the data types Redis supports. Redis keys are binary-coded strings with a maximum length of 512 MB, serving as identifiers for the associated values. String Strings are simple byte sequences with no restrictions on special characters, so they can hold nearly any content: text, integers, floating-point numbers, images, videos, and other media files, with a maximum size of 512 MB. Example: redis 127.0.0.1:6379> SET name "educativeOK redis 127.0.0.1:6379> GET name "educative" In this example, name is the key, and educative is the string value stored in the database. List Lists in Redis are ordered sequences of strings, sorted in insertion order. The system can efficiently handle lists with both small (500) and large (50,000) volumes of interconnected entries, making it ideal for processing large datasets. Example of creating lists: LPUSH mylist x   # list is now "x"LPUSH mylist y   # list is now "y","x"RPUSH mylist z   # list is now "y","x","z" (using RPUSH this time) Set Sets are similar to lists but unordered, and duplicates are not allowed. This storage method is useful when uniqueness is important but sequence order does not matter. Sorted Set Sorted sets allow users to choose the sorting criteria. If two elements have the same score, Redis will order them lexicographically. Each element is associated with a score, determining the set's ordering. Hash The hash data type stores field-value pairs. A Redis hash can contain millions of objects within a compact structure, making it suitable for use cases involving large numbers of entries or accounts in a single database. Example usage: HMSET user:1000 username antirez password P1pp0 age 34HGETALL user:1000HSET user:1000 password 12345HGETALL user:1000 Managing Redis Databases By default, Redis includes 16 isolated databases, each numbered from 0 to 15, with isolation ensuring that commands affect only the selected database, not others. By default, Redis connects to database 0. You can switch databases using the SELECT command after connecting. For instance, to switch to database 10: select 10 The selected database will then be indicated in the console prompt like this: 127.0.0.1:6379[10]˃ If you’re working in the default database 0, the prompt won’t show a database number: 127.0.0.1:6379˃ Copying Data Between Databases The swapdb command allows you to swap data between two databases, fully replacing the contents of the target database with those of the source database. For example, to swap databases 1 and 2: swapdb 1 2 If the operation is successful, Redis will display OK. Changes are applied immediately. Moving Keys Between Redis Instances The migrate command transfers a key from one Redis instance to another, removing it from the source instance. This command includes the following parameters in order: The target database’s host or IP address The target database’s port number The name of the key to be transferred The target database number (0–15) A timeout in milliseconds (maximum idle time allowed) For example: migrate 123.45.4.134 6379 key_1 6 8000 You can add one of the following options to the end of the migrate command: COPY: Retains the key in the source database while copying it to the target database. REPLACE: If the key already exists in the target database, it will be overwritten by the migrating key. KEYS: Instead of specifying a single key, you can provide a pattern after keys to transfer all matching keys, following patterns as outlined in the documentation. This enables bulk data transfers that match the specified pattern. Managing Keys Below are examples of several basic Redis commands for working with keys. To rename a key, use rename: rename old_key_name new_key_name The randomkey command is used to return a random key from the database: randomkey The output will display the key. The type command allows you to output the data type. The output will indicate one of the possible options: string, list, hash, set, zset, stream, or none — if such a key does not exist in the database. type key_name The move command transfers a key between databases within a single Redis instance (unlike migrate, which moves them to a different Redis instance). The command includes the key name and the target database file. For example, we will transfer data to database 6: move key_name 6 Upon successful execution, the output will show OK. Deleting Keys To delete one or more keys, use del: del key_name_1 key_name_2 If successful, the output will show (integer) 1. If something goes wrong, you will see (integer) 0. The unlink command is functionally similar to del but has some nuances. del temporarily blocks the client to free the memory occupied by a key. If this takes very little time (in the case of a small object), the blocking will likely go unnoticed. However, if the key is associated with a large number of objects, the deletion will take a considerable amount of time. During that time, any other operations will be impossible. Unlike del, the unlink command will first assess the cost of freeing the memory occupied by the key. If the costs are insignificant, unlink will behave like del, temporarily blocking the client. If the memory release requires significant resources, the deletion will occur asynchronously: unlink works in the background and gradually frees memory without blocking the client: unlink key_name In most cases, it is preferable to use unlink, as the ability to delete keys asynchronously and reduce errors due to blocking is a significant advantage. One of the following commands — flushdb or flushall — is used for bulk deletion of keys. Be very careful; this procedure occurs with no possibility of recovery (applicable to one or several databases). To delete all keys in the current database, use: flushdb To remove all keys across all databases on the Redis platform, use: flushall Both commands have an asynchronous deletion mode; add the async option to enable this. In this case, they will behave like unlink, gradually cleaning up memory in the background while other operations continue. Backup To create a backup of the current Redis database, you can use: save As a result, a snapshot of the current information is exported to a .rdb file. It is important to note that save blocks all other clients accessing the database during its execution. Therefore, the official documentation recommends running the save command only in a testing environment. Instead, it is suggested to use bgsave. This command informs Redis to create a fork of the database: the parent process will continue to serve clients, while the child process will unload the database backup. If changes are made during the execution of the bgsave command, they will not be included in the snapshot. bgsave You can also configure automatic regular snapshots that will occur when a certain number of changes have been made to the database. This creates a "save point." By default, the following settings for save points are specified in the redis.conf configuration file: /etc/redis/redis.conf...save 900 1save 300 10save 60 10000...dbfilename "nextfile.rdb"... According to this configuration, Redis will dump a snapshot of the database to the file specified in the dbfilename line at intervals of 900 seconds if at least 1 key has changed; 300 seconds if 10 or more keys have changed; and every 60 seconds if 10,000 or more keys have changed. Another command for creating a backup is shutdown. It will block every client connected to the database, perform a save, and close the connection. It is important to consider that this command will operate similarly to save, meaning: A snapshot will be created only if a save point is configured. During the blocking of clients while the shutdown command is executed, the necessary data may become unavailable to users or applications. It should be used only in a testing environment and when you are fully confident that you can safely block all server clients. shutdown If a save point is not configured but you want to create a snapshot, add save to the command: shutdown save You can also bypass creating a backup if a save point is configured, but you need to turn off the server without saving. For this, use: shutdown nosave Additionally, the shutdown command resets changes in the append-only file (the content does not change, and all new data is added to the end) before shutting down, if the AOF (Append Only File) function is enabled. This logs all write operations to the storage in a .aof file. The AOF and RDB modes can be enabled simultaneously, and using both methods is an effective backup strategy. The append-only file will gradually grow significantly. It is recommended to enable file rewriting considering certain variables. These settings should be specified within the redis.conf file. Another method for rewriting the append-only file is to execute the command: bgrewriteaof This will create a concise list of commands needed to roll back the database to its current state. bgrewriteaof operates in the background, but other background processes must be fully completed before it can be executed.
06 November 2024 · 9 min to read
Redis

Redis: Getting Started and Basic Commands

Redis is one of the most popular modern DBMSs (database management systems). In this Redis beginner tutorial, we will discuss the Redis basics: main features, data types, and commands, and we will also discuss the advantages of data caching using this DBMS. Getting to know Redis Redis is a non-relational DBMS, which means it works not only with table values but also with other data types, such as strings, lists, hashes, and sets. Data processing is implemented using the key-value principle. There is no SQL language in Redis, but you can use Lua scripts. This DBMS also features increased performance since data is stored directly in the server's RAM (in-memory), allowing you to perform more operations. Redis use cases The Redis DBMS is designed primarily to perform the following tasks (but is not limited to them): Storing user sessions, including fragments of website pages and several other elements (for example, the contents of an online store's cart, so when you return to the site and see that there are still items in the cart, it is probably implemented using Redis). Storing such data types as messages on users' "walls" on social networks, voting, results in tabular form, etc. Creating news feeds, group chats, blogs. Data caching which allows you to significantly reduce the load on a relational DBMS if it is used along with Redis. Storing data that needs to be quickly accessed, such as analytical, commercial, and other important information. Thus, using Redis, you can implement data transfer from various sensors that take and transmit readings from industrial equipment in real time. Getting started with Redis To run Redis, you first need to download it. The latest version of the DBMS is available on the official page. By default, Redis only supports Ubuntu and MacOS, but running on Windows is also possible and can be done in different ways, for example, using Docker or with the Chocolatey package manager. (After installing Chocolatey, you can easily search for the required version of Redis.) Redis itself is launched using the redis-server command. Then you can check that the installation is successful by using redis-cli. This output indicates that the DBMS is installed correctly: 127.0.0.1:6379> pingPONG Redis Basics: Data Types and Commands In this part, we will examine the main data types and commands used when working with Redis. This DBMS contains quite a few of them, so let's get acquainted with the most important ones. First, let's discuss the "building blocks" that form the basis of the entire system: the keys. Keys Keys in Redis are unique identifiers of the values associated with them. Values can be different: integers, strings, and even objects containing other nested values. Keys are used as pointers indicating where data is stored. We can draw an analogy with lockers where people can temporarily put things. The value is what is in this locker, and to access it, you need a specific key. Strings Let's move on to data types. String is the base data type that contains all other data. Strings in Redis are similar in function to strings in programming languages. The maximum allowed string size in Redis is 512 MB. Lists A list is a sequence of values that are arranged in a list in the order they were created. To create a list, let's get acquainted with some commands. LPUSH adds an element and LRANGE is used to display a list on the left. Enter the commands indicating list elements: LPUSH obj1 element1 (integer) 1 LPUSH obj1 element2 (integer) 2 LPUSH obj1 element3 (integer) 3 LRANGE obj1 0 -1 And we get the following output: 1) "element3" 2) "element2" 3) "element1" Hashes The essence of hashes or hash tables will be immediately clear to those who have programmed in Python or JavaScript. Dictionaries are very similar to hashes in Python, and objects are very similar to hashes in JavaScript. Redis uses the HSET command to write a value to a hash, and to read it, HGET. Example: HSET obj att1 val1 (integer) 1 HSET obj att2 val2 (integer) 1 HGET obj att1 "val1" If you need to get all the values, use the HGETALL instruction: HGETALL obj 1) "att1" 2) "val1" 3) "att2" 4) "val2" Sets In Redis, a set is an unordered collection of unique elements. To add another element there, enter the SADD command: SADD objects object1 (integer) 1 SADD objects object2 (integer) 1 SADD objects object3 (integer) 1 SADD objects object1 (integer) 0 Now, to get all the elements, you need to enter the SMEMBERS instruction: SMEMBERS objects 1) "object2" 2) "object3" 3) "object1" There are also other Redis commands for working with sets. For example, SUNION allows you to combine them. Sorted sets To add an element to a sorted set, use the ZADD command: ZADD objects 1230 val1 (integer) 1 ZADD objects 1231 val2 (integer) 1 ZADD objects 1232 val3 (integer) 1 Now, using the ZRANGE command, we get a slice: ZRANGE objects 0 -1 1) "val1" 2) "val2" 3) "val3" Other Redis commands The following commands will also be helpful for Redis beginners to work with keys: The HKEYS command prints all the keys recorded in the hash. Let's first write the values into a hash and then output the keys: HSET object1 type "table" (integer) 1 HSET object1 dimensions 75-50-50 (integer) 1 HKEYS object1 1) "type" 2) "dimensions" If we need to display all the values, the HVALS command will help: HVALS object1 1) "table" 2) "75-50-50" The EXISTS command is used to check the existence of a key. If it exists, 1 is output, if not, then 0. For example: EXISTS dimensions (integer) 1 EXISTS instructions (integer) 0 To rename a key, use the RENAME command. First, enter the key that should be renamed and then its new name: RENAME dimensions profile OK HKEYS object1 1) "type" 2) "profile" And to delete a key (along with its value), use the DEL command: DEL profile (integer) 1 HKEYS object1 1) "type" Caching in Redis One of the problems that Redis solves is efficient data caching. Redis is often used together with relational DBMSs, such as PostgreSQL. Caching allows you to quickly load small objects that are frequently updated while minimizing the risk of information loss. Redis serves as a buffer DBMS and checks the key in response to a user request without affecting the main database. This significantly reduces the load on resources with high traffic (from several thousand users per hour). To organize caching, you need to include the appropriate libraries, and you can use various programming languages. For example, in Python this is done through the import function: import redisimport sqlite Then the connection to the SQL database is configured: def get_my_friends(): connection = sqlite.connect(database="database.db") cursor = connection.cursor() And then the DBMS is asked for the presence of the key: redis_client = redis.Redis() Pub/Sub Channels in Redis Redis has a subscription mechanism which is implemented using channels. Published messages can be read by clients who have subscribed to the channel. Technically, this is similar to a regular chat, which can be useful for a group of developers. However, there is no guarantee that messages sent through such a channel will be read. Subscription to a channel is made using the SUBSCRIBE command, followed by the name of the channel, for example: SUBSCRIBE hostman_channel Messages are published using the PUBLISH command: PUBLISH hostman_channel "Hello, we have launched a new channel!"(integer) 2 The returned value (2) is the number of subscribers who received the message. Conclusion In this article, we have discussed how Redis works, learned the basic commands, and how to work with several types of data and special features, such as caching and channels. For a more in-depth study of Redis' capabilities, we recommend reading the official documentation. You can also find books and free guides online that describe advanced methods for working with Redis.
03 April 2024 · 7 min to read

Do you have questions,
comments, or concerns?

Our professionals are available to assist you at any moment,
whether you need help or are just unsure of where to start.
Email us
Hostman's Support