Web Application Caching in Node.JS

by Michael Szul on

Coming from the .NET community, when I started to do more work in Node.JS (and the Express framework in particular), there was one area that I missed dearly: Application Runtime Caching.

I know; It's a crutch. But Application Runtime Caching is one surefire way to speed up application performance when you need to cut down on database query calls and the data you're pulling is allowed to be behind its real-time self for a bit. Modern web architectures prefer to use solutions like memcached or Redis, but sometimes you need a simpler solution due to externalities (e.g., budgets, experience).

So in ASP.NET, you have the Application Runtime Cache, but what do we have in Node.JS?

There are a variety of Node.JS caching package out there, but we're going to concentrate on node-cache, which is one of the simplest to implement. And since we're talking about storing data selected from the database, we'll do that with the mssql package for Microsoft SQL Server databases.

First, we'll import both packages:

import * as NodeCache from 'node-cache';
      import { ConnectionPool, Int } from 'mssql';
      import { CONNECTION, Assessor } from '../schema';
      

Some things to ignore: The imports from schema are simply pulling in the database connection information and an interface that will be used to define the database selection return result. You don't need to worry about these. If you feel you need to know more about the mssql package, I wrote an introductory post on it a while back.

Next, we'll want to create the caching object:

const cache = new NodeCache({
          stdTTL: 3600,
          checkperiod: (3600 * 0.5)
      });
      

The first option is the number of seconds to hold the data in the cache, and the second option is the time before checking for expired data and removing it from the cache.

Now that we have a cache object, we can use it. The standard syntax for setting the cache and getting from the cache is as follows:

//Set item in cache
      cache.set(key, val);
      
      //Get item from cache
      const val= cache.get(key);
      

You can also delete items from the cache by using the key as well:

//Delete from the cache
      cache.del(key);
      

This works even better when you abstract it into a class. I've added the full example on GitHub as a Gist, but for this post, we'll take a look explicitly at the get() function that we've wrapped around the standard cache.get() function. Here's the implementation (as a class method off the aforementioned abstraction):

...
      public async get(key: string, callback?: Function): Promise<any> {
          const value: any = this._cache.get(key);
          if (value != null) {
              return Promise.resolve(value);
          }
          try {
              if(callback != null) {
                  const result: any = await callback();
                  this._cache.set(key, result);
                  return Promise.resolve(result);
              }
              return Promise.resolve(null);
          }
          catch(e) {
              Promise.reject(e);
          }
      }
      ...
      

This code allows us to combine get() and set() from the cache package so that if the get() doesn't return any data, we set the value based on the result of a callback function. As you can see, the first parameter is the cache key, while the second parameter is an optional asynchronous callback function.

That's great, but what does this look like in usage? Let's tie it into a SQL query to see how we can store database data in the cache:

export async function getAssessors(
          AssessorID: number
      ): Promise<Assessor[]> {
          try {
              const data: Promise<Assessor[]> = await cache.get(`lov_getAssessors_${ AssessorID }`, async (): Promise<Assessor[]> => {
                  const pool: ConnectionPool = await new ConnectionPool(CONNECTION).connect();
                  try {
                      const result: IResult<any> = await pool.request()
                          .input('AssessorID', Int, AssessorID)
                          .query(assessorSelectQuery);
                      return Promise.resolve(result.recordset);
                  }
                  catch(e) {
                      return Promise.reject(e);
                  }
              });
              return data;
          }
          catch(e) {
              return Promise.reject(e);
          }
      }
      

Note that assessorSelectQuery is a variable that represents the SQL being execute. This is left out on purpose. Additionally, Assessor is the interface from the imported schema file that we're using to describe the data.

This combines standard Microsoft SQL Server Node.JS selection code with a cache call. We're setting data to the result of cache.get(), which has a key, and also has an anonymous callback function. This is the function we await in the abstracted get() function to retrieve the data, and it returns the record set from the database. With this code, we essentially have a data call to get "assessors" that first checks the cache, and if the cache doesn't have an entry, it executes the database call, stores the result in the cache for use later, and then returns the result for immediate use.

With this, we're able to use a caching mechanism similar to ASP.NET's Application Runtime Cache in philosophy. Just don't go shoving the world into it.