Using Redis Client in NestJS
Tags: nestjs, redis, node, typescript
Published: 2023-09-27 21:48:45

In this article, we will explore how to set up a Redis client in a NestJS application for caching and storing short-lived data. We will provide code snippets and explanations for each step of the process.

## Setting Up the Redis Client

First, we need to install the Nest CLI by running the following command in your terminal:

```bash
npm install -g @nestjs/cli
```

Next, create a new NestJS project using the following command, replacing `<your-project-name>` with your desired project name:

```bash
nest new <your-project-name>
```

This project will focus on two primary uses of Redis: caching and storing short-lived data. We'll use Node.js version 18 and the `ioredis` library to connect to the Redis service.

### Setting Up the Redis Client Factory

To interact with Redis, we need to set up a Redis client factory that will create a Redis connection instance used throughout the code. Here's an example of the Redis client factory:

```typescript
import { FactoryProvider } from '@nestjs/common';
import { Redis } from 'ioredis';

export const redisClientFactory: FactoryProvider<Redis> = {
    provide: 'RedisClient',
    useFactory: () => {
        const redisInstance = new Redis({
            host: process.env.REDIS_HOST,
            port: +process.env.REDIS_PORT,
        });

        redisInstance.on('error', e => {
            throw new Error(`Redis connection failed: ${e}`);
        });

        return redisInstance;
    },
    inject: [],
};
```

Please note that in this example, we are not implementing any connection retry mechanism. An error will be thrown if there's an issue with the Redis connection, which may not be the best practice but suits our use case here.

### Setting Up the Redis Repository

We will set up a Redis repository to abstract our Redis client and provide methods for interacting with the Redis store in the application layer. Here's an example of the Redis repository:

```typescript
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
import { Redis } from 'ioredis';
import { RedisRepositoryInterface } from '../../../domain/interface/redis.repository.interface';

@Injectable()
export class RedisRepository implements OnModuleDestroy, RedisRepositoryInterface {
    constructor(@Inject('RedisClient') private readonly redisClient: Redis) {}

    onModuleDestroy(): void {
        this.redisClient.disconnect();
    }

    async get(prefix: string, key: string): Promise<string | null> {
        return this.redisClient.get(`${prefix}:${key}`);
    }

    async set(prefix: string, key: string, value: string): Promise<void> {
        await this.redisClient.set(`${prefix}:${key}`, value);
    }

    async delete(prefix: string, key: string): Promise<void> {
        await this.redisClient.del(`${prefix}:${key}`);
    }

    async setWithExpiry(prefix: string, key: string, value: string, expiry: number): Promise<void> {
        await this.redisClient.set(`${prefix}:${key}`, value, 'EX', expiry);
    }
}
```

We use the NestJS `onModuleDestroy` lifecycle event method to close the Redis connection when the application is torn down. This event is triggered when `app.close()` is called or when the process receives a special system signal, such as `SIGTERM`, if `enableShutdownHooks` is called during application bootstrap.

As you can see, we use a prefix for all keys stored in Redis. This allows us to create a folder-like structure when accessing the Redis store with applications like RedisInsight, using the colon (':') as the delimiter. The prefix is defined as an enum value containing all the data stores (collections/tables) used in the project.

### Setting Up the Redis Service

Finally, we create a Redis service to be used in the application layer. The Redis service encapsulates the Redis repository and provides the methods needed in the application layer. Here's an example of the Redis service:

```typescript
import { Inject, Injectable } from '@nestjs/common';
import { RedisPrefixEnum } from '../domain/enum/redis-prefix-enum';
import { ProductInterface } from '../domain/interface/product.interface';
import { RedisRepository } from '../infrastructure/redis/repository/redis.repository';

const oneDayInSeconds = 60 * 60 * 24;
const tenMinutesInSeconds = 60 * 10;

@Injectable()
export class RedisService {
    constructor(@Inject(RedisRepository) private readonly redisRepository: RedisRepository) {}

    async saveProduct(productId: string, productData: ProductInterface): Promise<void> {
        // Expiry is set to 1 day
        await this.redisRepository.setWithExpiry(
            RedisPrefixEnum.PRODUCT,
            productId,
            JSON.stringify(productData),
            oneDayInSeconds,
        );
    }

    async getProduct(productId: string): Promise<ProductInterface | null> {
        const product = await this.redisRepository.get(RedisPrefixEnum.PRODUCT, productId);
        return JSON.parse(product);
    }

    async saveResetToken(userId: string, token: string): Promise<void> {
        // Expiry is set to 10 minutes
        await this.redisRepository.setWithExpiry(
            RedisPrefixEnum.RESET_TOKEN,
            token,
            userId,
            tenMinutesInSeconds,
        );
    }

    async getResetToken(token: string): Promise<string> {
        return await this.redisRepository.get(RedisPrefixEnum.RESET_TOKEN, token);
    }
}
```

In this service, we use the `RedisPrefixEnum` as the collection/table name. We have implemented four methods: `saveProduct` to cache a product for a day, `saveResetToken` to store a reset token for ten minutes, and `getProduct` and `getResetToken` to retrieve stored product and token values, respectively.

## Using Redis Service in Other Services

Now, let's see how we can use the Redis service in our password reset service and product service.

### Password Reset Service

Here's an example of the password reset service using the Redis service:

```typescript
import { Inject, Injectable } from '@nestjs/common';
import { ResetTokenInterface } from '../domain/interface/reset.token.interface';
import { RedisService } from './redis.service';

@Injectable()
export class PasswordResetService {
    constructor(@Inject(RedisService) private readonly redisService: RedisService) {}

    async generateResetToken(userId: string): Promise<ResetTokenInterface> {
        // Check if the user exists in the database
        // Generate a random number token with a length of 6
        const token = Math.floor(100000 + Math.random() * 900000).toString();
        await this.redisService.saveResetToken(userId, token);
        return { token };
    }

    async getTokenUserId(token: string): Promise<string | null> {
        return await this.redisService.getResetToken(token);
    }
}
```

### Product Service

Here's an example of the product service using the Redis service:

```typescript
import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common';
import { ProductInterface } from '../domain/interface/product.interface';
import { RedisService } from './redis.service';

// We will be using a dummy JSON product API to fetch our product data
// This could be any API call or database operation
const productURL = 'https://dummyjson.com/products/';

@Injectable()
export class ProductService {
    constructor(@Inject(RedisService) private readonly

 redisService: RedisService) {}

    async getProduct(productId: string): Promise<any> {
        // Check if the product exists in Redis
        const product = await this.redisService.getProduct(productId);
        if (product) {
            console.log('Cache hit! Product found in Redis');
            return { data: product };
        }

        const res = await fetch(`${productURL}${productId}`);

        if (res.ok) {
            const product: ProductInterface = await res.json();
            // Cache the data in Redis
            await this.redisService.saveProduct(`${product.id}`, product);
            console.log('Cache miss! Product not found in Redis');
            return product;
        } else {
            throw new InternalServerErrorException('Something went wrong');
        }
    }
}
```

These services can be injected into other services or controllers. To keep this article concise, we have not included the controller code, but you can find it in the [code repository](https://github.com/IdrisAkintobi/nestjs-with-redis.git).

## Conclusion

In this article, we explored how to set up a Redis client in a NestJS application for caching and storing short-lived data. We created a Redis client factory, a Redis repository, and a Redis service to abstract the Redis interactions in the application layer. We also demonstrated how to use the Redis service in password reset and product services to enhance data retrieval and caching. By implementing the techniques and structures outlined in this article, you can seamlessly incorporate Redis into your own application, optimizing its performance and enhancing data management.