Otimize seu Aplicativo com Cache de Dados usando NestJS e Redis
Tags: node, redis, nestjs, mongodb
Published: 2024-04-13 03:41:15

No mundo de desenvolvimento de software, construir aplicações robustas e performáticas são de suma importância.

Pense no seguinte cenário: digamos que você tenha um aplicação de consulta de usuários, e toda vez que alguém entra no perfil de determinado usuário, é preciso realizar uma consulta no banco de dados, certo? E aí vai retornar dados como:

> _nome, data de nascimento, estado civil, altura, peso, formação, etc._

Raramente essas informações vão mudar, certo? Como evitar que seu servidor consulte sempre o banco de dados?

A resposta é simples: CACHE!

A técnica de cache se baseia no princípio de armazenar temporariamente dados frequentemente acessados em uma área de armazenamento de acesso mais rápido, como a memória RAM, para reduzir o tempo necessário para recuperar esses dados em comparação com a obtenção dos dados diretamente da fonte original, como um banco de dados ou uma API externa.

Vamos utilizar uma ótima solução para realizar essa técnica, o `Redis`. 

> **Redis é um armazenamento de estrutura de dados em memória, usado como um banco de dados em memória distribuído de chave-valor, cache e agente de mensagens, com durabilidade opcional extremamente rápido.**

### Desenho da arquitetura

Inicialmente sem cache, sua modelagem fica no seguinte modelo:

![Arquitetura Microserviço sem cache](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8mbc1x1gz4xl0qkksy44.png)

Após o cache, ficará assim:

![Arquitetura Microserviço com cache](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dpkzqd1qg9pxsk15cvnt.png)

Para realizar o cache é necessário realizar pelo menos uma ou mais vezes consulta no banco de dados, nunca dá pra saber quando o usuário vai querer mudar seus dados, então, quando o servidor realizar uma consulta no Banco de Dados `(3)`, ele irá armazenar esses dados no Redis `(4)`.

A partir desse momento você poderá consultar no Redis os dados:

![Desenho arquitetura obtendo do redis](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lzp89vk7dxjovm1tbnks.png)

- Consulta do Redis, se não encontrou os dados:
    - Consulta Banco de Dados
    - Armazena os dados no Redis
    - retorna os dados

- Consulta do Redis, se encontrou os dados:
    - retorna os dados

## Iniciando o Projeto

Para esse exemplo, estarei utilizando uma aplicação **NestJS + Redis + MongoDB**, mas você pode realizar com outros frameworks, como Java utilizando Spring Data Redis + H2, ou GoLang utilizando go-redis + POSTGRES, entre muitas outras.

Para iniciar um aplicativo NestJS, é necessário ter instalado o CLI do Nest:

`$ npm install -g @nestjs/cli`

Para criar um projeto:

`nest new cache-with-redis`

Estarei utilizando o Mongoose, como TypeORM para se conectar ao Banco de dados MongoDB.

`$ npm i @nestjs/mongoose mongoose`

Para o redis, estarei utilizando um Redis Módulo para o Nest:

`$ npm install @liaoliaots/nestjs-redis ioredis`

Para configurar o projeto utilizando variáveis de ambiente, estarei instalando um módulo do Nest para configuração:

`$ npm i --save @nestjs/config`

Vamos iniciar configurando um módulo para o Redis, comece criando 2 arquivos:

`redis-cache.module.ts`
`redis-repository.ts`

```ts
// redis-cache.module.ts
import { RedisModule } from "@liaoliaots/nestjs-redis";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { RedisCacheRepository } from "./redis-cache.repository";

@Module({
    imports: [
        ConfigModule.forRoot(),
        RedisModule.forRoot({
            config: {
                host: process.env.REDIS_HOST,
                port: Number(process.env.REDIS_PORT),
                password: process.env.REDIS_PASSWORD
            }
        })
    ],
    providers: [RedisCacheRepository],
    exports: [RedisCacheRepository]
})
export class CacheRedisModule {}
```

```ts
// redis-cache.repository.ts
import { InjectRedis } from "@liaoliaots/nestjs-redis";
import { Injectable } from "@nestjs/common";
import Redis from 'ioredis';

@Injectable()
export class RedisCacheRepository {
    constructor(@InjectRedis() private readonly redis: Redis) {}

    async saveData<T>(data: T, key: string): Promise<void> {
        // key -> chave onde o redis irá salvar os dados
        // JSON.stringify(data) -> salvar os dados em JSON
        // EX -> utilizado para indicar que o tempo será passado em segundos
        // 180 -> 180 segundos = 3 minutos de cache
        // Time Complexity -> O(1)
        await this.redis.set(key, JSON.stringify(data), "EX", 180)
    }

    async getData<T>(key: string): Promise<T> {
        // retorna o dado salvo da chave
        // Time Complexity -> O(1)
        return JSON.parse(await this.redis.get(key)) as T;
    }
}
```

Agora iremos cuidar da nossa entidade Usuário e seu repositório utilizando MongoDB, crie 5 arquivos:

`models/user.entity.ts`
`user.repository.ts`
`user.servive.ts`
`user.controller.ts`
`user.module.ts`

```ts
// user.entity.ts
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from "mongoose";

export type UserDocument = HydratedDocument<User>;

export enum GenderEnum {
    FEMALE = "Female",
    MALE = "Male"
}

export enum MaritalStatusEnum {
    SINGLE = "Single",
    MARRIED = "Married",
    Divorced = "Divorced",
    Widowed = "Widowed",
    Separated = "Separated"
}

@Schema()
export class User {

    @Prop()
    name: string;

    @Prop()
    age: number;

    @Prop({ type: String, enum: GenderEnum })
    gender: GenderEnum

    @Prop({ type: String, enum: MaritalStatusEnum })
    maritalStatus: MaritalStatusEnum;

    @Prop()
    height: number;
}

export const UserSchema = SchemaFactory.createForClass(User);


```

```ts
// user.repository.ts
import { Injectable } from "@nestjs/common";
import { InjectModel } from "@nestjs/mongoose";
import { User } from "./models/user.entity";
import { Model } from "mongoose";
import { CreateUserDto } from "./dto/create-user.dto";

@Injectable()
export class UserRepository {
    constructor(@InjectModel(User.name) private readonly userModel: Model<User>) {}

    async create(createUserDto: CreateUserDto): Promise<User> {
        const userCreated = new this.userModel(createUserDto);
        return userCreated.save();
    }

    async findById(userId: string): Promise<User> {
        return await this.userModel.findById(userId);
    }
}
```

```ts
// user.servive.ts
import { Injectable } from "@nestjs/common";
import { UserRepository } from "./user.repository";
import { RedisCacheRepository } from "src/redis-cache/redis-cache.repository";
import { CreateUserDto } from "./dto/create-user.dto";

@Injectable()
export class UserService {
    constructor(
        private readonly userRepository: UserRepository,
        private readonly redisCacheRepository: RedisCacheRepository
    ) {}

    async getUserById(userId: string) {
        const userCache = await this.redisCacheRepository.getData(userId);

        if (userCache) {
            return userCache;
        }

        const userRepository = await this.userRepository.findById(userId);

        await this.redisCacheRepository.saveData(userRepository._id, userRepository);

        return userRepository;
    }

    async saveUser(userCreateDto: CreateUserDto) {
        return await this.userRepository.create(userCreateDto);
    }
}
```

```ts
// user.controller.ts
import { Body, Controller, Get, HttpStatus, Param, Post, Res } from "@nestjs/common";
import { UserService } from "./user.service";
import { CreateUserDto } from "./dto/create-user.dto";
import { Response } from "express";

@Controller("user")
export class UserController {
    constructor(private readonly userService: UserService) {}

    @Post()
    async postUser(@Body() userDto: CreateUserDto, @Res() res: Response) {
        const userCreated = await this.userService.saveUser(userDto);

        res.status(HttpStatus.OK).json(userCreated);
    }

    @Get(":id")
    async get(@Param("id") id: string, @Res() res: Response) {
        const user = await this.userService.getUserById(id);
        
        res.status(HttpStatus.OK).json(user);
    }
}
```

É importante criar um arquivo `.env` no root do projeto:

```shell
# .env
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=password
MONGODB_HOST="mongodb://127.0.0.1:27017/CACHE_WITH_REDIS"
```

Pronto, a aplicação está pronta para ser executada. Você pode executar executando o comando:

`$ npm run start`

Logo após verá um saída dessa forma:

![Log Running App](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y8ww0in237oc2o2may6n.png)

Para salvar um usuário, podemos realizar uma requisição do tipo `POST` com o `curl`:

```shell
curl --location 'localhost:3000/user' \
--header 'Content-Type: application/json' \
--data '{
  "name": "John Doe",
  "age": 30,
  "gender": "Male",
  "maritalStatus": "Married",
  "height": 1.80
}'
```

Logo após retornará algo do gênero:

![Response create user](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/25g0uf28lqeuup9w259h.png)

### Redis em Ação

Agora veremos o Redis em Ação, ao realizar uma requisição do método `GET` para obter o usuário:

```shell
curl --location 'localhost:3000/user/6619fa598d1a2e8cb93a95f3'
```

Vamos obter o seguinte resultado:

![Response get user by id](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/11dd0s2h8vzsw75t44j3.png)

Repare que a resposta volta com o seguinte tempo de resposta: `34ms`. De acordo com nossa regra de negócio, ele faz uma consulta no Redis, se não achar, ele vai no Banco de Dados(MongoDB), logo após **salva no redis** e retorna, certo? Boa, vamos ver se salvou no redis?

```cli
# Redis CLI
> GET 6619fa598d1a2e8cb93a95f3
"{\"_id\":\"6619fa598d1a2e8cb93a95f3\",\"name\":\"John Doe\",\"age\":30,\"gender\":\"Male\",\"maritalStatus\":\"Married\",\"height\":1.8,\"__v\":0}"
```

Olha que legal, agora o nosso dado está salvo no Redis, vamos ver se melhorou o tempo de resposta da API?

![Response get user by id with cache](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jpk13yl1yn61fs2fn4wp.png)

`4ms`, isso é um absurdo de rápido. Você deve estar pensando: "Caramba, só 29ms de diferença, isso não faz diferença."
Em ambientes escaláveis e robustos que necessitam de sistemas rápidos, como o PIX, qualquer diferença de tempo muda. Imagine quando você precisar cachear o resultado de alguma API externa em que você utiliza que não muda muito o resultado, seria de grande utilidade, certo? 

Chegamos ao final. Espero que tenham gostado, estarei deixando alguns links de referência:

[Configuration NestJS](https://docs.nestjs.com/techniques/configuration)
[MongoDB NestJS](https://docs.nestjs.com/techniques/mongodb)
[Redis Module NestJS](https://github.com/liaoliaots/nestjs-redis)
[SET command Redis](https://redis.io/docs/latest/commands/set/)
[GET command Redis](https://redis.io/docs/latest/commands/get/)

O link do repositório utilizado no tutorial se encontra aqui:
[Projeto Repositório](https://github.com/jusemarinho/nestjs-cache-with-redis)

Um Abraço e bons estudos! Até mais!