Adding Redis


In order to save (or persist) data over a duration longer than a single request, we need some long term storage solution to keep hold of our data. This could be a database like Postgres. In our case, this will be Redis.

You can setup and configure Redis however you like. For simplicity, I'm going to use Docker.

touch docker-compose.yaml

Into this file:

version: "3"

services:
  redis:
    image: redis:5.0.5
    ports:
      - "6401:6379"

To make this work, you will need Docker, and docker-compose installed on your system.

If you're new to Docker, consider checking out my Docker tutorial for beginners.

Essentially this will give us an instance of Redis, with the publicly available port of 6401 that maps to the internal / common Redis port of 6379. I do this as if I have multiple instances of Redis up and running in different projects, I don't end up with port conflicts by assuming each project has access to the default port.

With this in place:

docker-compose up

And we are away.

Again, at the start of this tutorial series we included redis, and @types/redis in our package.json file. We therefore already have the node compatible library locally available and ready for us to use.

Persistence

Our persistence or storage layer is going to use Redis.

This layer will be extremely simple. We will have three operations:

  • get
  • add
  • remove

In other words, we can get the entire list, add an item to the list, and remove all items from the list.

The bigger picture says we could implement this storage layer in multiple architectures. We will do the Redis variant. But we could just as easily do a Postgres variant, or a Mongo (shudder) variant... or any other type of persistent storage implementation.

All we need is something that implements this particular interface.

Let's define this specification as a TypeScript interface.

First we need a file to hold our Redis implementation:

mkdir src/storage
touch src/storage/redis.ts

Into which I will start off by defining the interface:

interface IStorage {
  get: (list: string) => string[];
  add: (list: string, name: string) => boolean;
  remove: (list: string, name: string) => boolean;
}

This initial attempt is a little naive, as we shall soon see.

There is a more immediate concern, however.

Let's say we want to add a variation. We maybe want to add in src/storage/postgres.ts which provides a SQL-backed variant of this same interface.

How would we share this IStorage interface definition?

Well, we'd need to export it:

export interface IStorage {
  // ...

But meh. Big meh, right?

Why would we want to do import * as Interfaces from './redis'; (or similar) from our Postgres implementation? That would be weird.

Instead, I personally prefer to move all my interfaces, and other definitions, out of individual files, and into a centralised file I call src/types/interfaces.d.ts (or similar).

This file will contain all our interface definitions. One place. One file. Simple.

mkdir src/types
touch src/types/interfaces.d.ts

Into which I will extract out the two interfaces we have defined so far:

export interface IConfig {
  port: string;
}

export interface IStorage {
  get: (list: string) => string[];
  add: (list: string, name: string) => boolean;
  remove: (list: string, name: string) => boolean;
}

Be sure to update both files, e.g.:

import * as Interfaces from './types/interfaces';

export const config: Interfaces.IConfig = {
  port: process.env.PORT || '7654',
};

And our new redis.ts file:

import * as Interfaces from '../types/interfaces';

export const redisStorage: Interfaces.IStorage = {
  get: (list: string) => [],
  add: (list: string, name: string) => false,
  remove: (list: string, name: string) => false
};

Note the basic implementation has to meet the interface.

Here's the Redis implementation of the IStorage interface we have by the end of this video:

import { config } from "../config";
import * as Interfaces from '../types/interfaces';

const redis = require("redis");
const client = redis.createClient(config.redis);

const {promisify} = require('util');

const rpush = promisify(client.rpush).bind(client);
const lrem = promisify(client.lrem).bind(client);
const lrange = promisify(client.lrange).bind(client);

export const redisStorage: Interfaces.IStorage = {
  get: (list: string) => {
    return lrange(list, 0, -1).then(val => val).catch(e => []);
  },
  add: (list: string, name: string) => {
    return rpush(list, name).then(val => val > 0).catch(e => false);
  },
  remove: (list: string, name: string) => {
    return lrem(list, 0, name).then(val => val > 0).catch(e => false);
  }
};

This requires a change to IStorage:

export interface IStorage {
  get: (list: string) => Promise<string[]>;
  add: (list: string, name: string) => Promise<boolean>;
  remove: (list: string, name: string) => Promise<boolean>;
};

As covered in the video, there is also a change to the IConfig interface and therefore to the config implementation itself. Watch the video for the full change.

With a persistence layer available to us, we can return to the implementation of our endpoint.

Episodes