With the Redis Hackathon, a lot of people will want to use the RedisOM module as their main Object Mapper, but how do you connect it with NestJS, my favourite NodeJS framework.
On this article I'll explain how to connect it by creating a basic Todo app. In the end of the article I'll also explain how to create a Dynamic Module for the RedisOM for the Microservice Mavens out-there.
Set Up
Since this article is about NestJS I'll assume you have the cli installed, if not click here. On your projects folder do the following commands set up a new NestJS project and open it in VSC (or any other code editor).
~ nest n redis-todo -s
~ code ./redis-todo
So we're all use the same node and package manager I recommend using Node 16 and Yarn. To set up yarn 3 with the node-modules folder create a file called .yarnrc.yml and inside write.
nodeLinker: node-modules
After to set up yarn version 3 and interactive tools:
~ yarn set version stable
~ yarn plugin import interactive-tools
For this app after installing and updating all instances we'll need to install RedisOM node and the config module.
~ yarn install
~ yarn upgrade-interactive
~ yarn add redis-om
On the tsconfig.json, at the end add esModuleInterop:
{
"compilerOptions": {
// ...
"esModuleInterop": true
}
}
Configuration
For configuration we'll use .env and NestJS Config Module, for this we'll install its dependency:
yarn add @nestjs/config joi
Create a folder called config, and a sub-folder inside called interfaces.
On the interfaces folder create a file with the configuration object called config.interface.ts:
export interface IConfig {
redisUrl: string;
port: number;
}
On the config folder we'll create a validation schema for our .env file and call it validation.schema.ts:
import Joi from 'joi';
export const validationSchema = Joi.object({
NODE_ENV: Joi.string().required(),
PORT: Joi.number().required(),
REDIS_URL: Joi.string().required(),
});
On the config folder create an index.ts file and add the config function in it.
import { IConfig } from './interfaces/config.interface';
export function config(): IConfig {
return {
port: parseInt(process.env.PORT, 10),
redisUrl: process.env.REDIS_URL,
};
}
export { validationSchema } from './validation.schema';
Optionally as seen above you can export the validation schema from there as well.
Finally just import your config module on the AppModule:
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { config, validationSchema } from './config';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema,
load: [config],
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Creating the RedisOM Client
Now generate both the module and service for the redis-om:
~ nest g mo redis-client
~ nest g s redis-client --no-spec
On the redis-client folder open the redis-client service and import the Client class from the redis-om package:
import { Injectable } from '@nestjs/common';
import { Client } from 'redis-om';
To be able to use the redis-om will extend the service with the Client Class:
// ...
@Injectable()
export class RedisClientService extends Client {}
To be able to connect to the client we need to open it when the module initializes, and close the client when you destroy it.
import { Injectable, OnModuleDestroy } from '@nestjs/common';
// ...
@Injectable()
export class RedisClientService
extends Client
implements OnModuleDestroy
{
constructor() {
super();
}
public async onModuleDestroy() {
//...
}
}
To be able to connect to the server we need to use the open method on the constructor with the redis url which we can get from the config service, and for closing we use the close method the onModuleDestroy method. So putting it all together:
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Client } from 'redis-om';
@Injectable()
export class RedisClientService extends Client implements OnModuleDestroy {
constructor(private readonly configService: ConfigService) {
super();
(async () => {
await this.open(configService.get<string>('redisUrl'));
})();
}
public async onModuleDestroy() {
await this.close();
}
}
Finally on the redis-client.module export the service and make the module global:
import { Global, Module } from '@nestjs/common';
import { RedisClientService } from './redis-client.service';
@Global()
@Module({
providers: [RedisClientService],
exports: [RedisClientService],
})
export class RedisClientModule {}
This is all the set-up necessery to use the redis-om node with NestJS, the rest of the article is a tutorial on how to use them within a REST API. If you skip to the end I added the code for the dynamic module library for the ones using NestJS micro-services.
TODO APP EXAMPLE
A todo app with a basic JWT authentication system using redis as its main database.
Basic Authentication System
Configuration Update:
Start by adding creating a jwt.interface.ts file on your config interfaces folder:
export interface IJwt {
time: number;
secret: string;
}
On you config interface add the jwt:
import { IJwt } from './jwt.interface';
export interface IConfig {
redisUrl: string;
port: number;
jwt: IJwt;
}
Finally change both the validation schema and the config function to have the jwt:
Validation schema:
// ...
export const validationSchema = Joi.object({
// ...
ACCESS_SECRET: Joi.string().required(),
ACCESS_TIME: Joi.number().required(),
});
Config function:
// ...
export function config(): IConfig {
return {
// ...
jwt: {
secret: process.env.ACCESS_SECRET,
time: parseInt(process.env.ACCESS_TIME, 10),
},
};
}
The Auth Service:
Start by using the cli to generate a REST Api resource for auth:
~ nest g res auth
When using this command don't create the CRUD end-points.
On the auth folder create a sub-folder called entities which will have the user.entity.ts. The User entity will be a basic redis entity with a schema as seen in the redis-om-node readme:
import { Entity, Schema } from 'redis-om';
export class User extends Entity {
name: string;
email: string;
password: string;
createdAt: Date;
}
export const userSchema = new Schema(User, {
name: { type: 'string' },
email: { type: 'string' },
password: { type: 'string' },
createdAt: { type: 'date' },
});
On the auth service need to inject the redis-client and the config-server as dependencies, and since we made redis-client and config global we don't need to touch the module for now:
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { RedisClientService } from '../redis-client/redis-client.service';
@Injectable()
export class AuthService {
constructor(
private readonly redisClient: RedisClientService,
private readonly configService: ConfigService,
) {}
}
After that we need to set up the user repository and start its index so we can use redis search (NOTE: This could go to the constructor inside an arrow function but I prefer adding the OnModuleInit), as well as the JWT time and secret:
import {
BadRequestException,
Injectable,
OnModuleInit,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Repository } from 'redis-om';
import { IJwt } from '../config/interfaces/jwt.interface';
import { RedisClientService } from '../redis-client/redis-client.service';
import { User, userSchema } from './entities/user.entity';
@Injectable()
export class AuthService implements OnModuleInit {
private readonly usersRepository: Repository<User>;
private readonly jwt: IJwt;
constructor(
private readonly redisClient: RedisClientService,
private readonly configService: ConfigService,
) {
this.usersRepository = redisClient.fetchRepository(userSchema);
this.jwt = configService.get<IJwt>('jwt');
}
public async onModuleInit() {
await this.usersRepository.createIndex();
}
}
We could use the Nestjs Jwt module but I find that creating the async jwt methods on the auth service is easier, so start by installing the jsonwebtoken and bcrypt packages:
~ yarn add jsonwebtoken bcrypt
~ yarn add -D @types/jsonwebtoken @types/bcrypt
Create a sub-directory called interfaces and add the access-id.interface.ts:
export interface IAccessId {
id: string;
}
export interface IAccessIdResponse extends IAccessId {
iat: number;
exp: number;
}
Then just add a private method for generation:
import {
//...
BadRequestException,
UnauthorizedException,
} from '@nestjs/common';
// ...
import { sign } from 'jsonwebtoken';
@Injectable()
export class AuthService implements OnModuleInit {
// ...
private async generateToken(user: User): Promise<string> {
return new Promise((resolve) => {
sign(
{ id: user.entityId },
this.jwt.secret,
{ expiresIn: this.jwt.time },
(error, token) => {
if (error)
throw new InternalServerErrorException('Something went wrong');
resolve(token);
},
);
});
}
}
Now we can start creating the dtos for the register, login and delete routes, so we need to install the class-validator:
~ yarn add class-validator class-transformer
Create the following files on a dto sub-folder:
Register dto (register.dto.ts):
import { IsEmail, IsString, Length, MinLength } from 'class-validator';
export abstract class RegisterDto {
@IsString()
@IsEmail()
@Length(7, 255)
public email: string;
@IsString()
@Length(3, 100)
public name: string;
@IsString()
@Length(8, 40)
public password1: string;
@IsString()
@MinLength(1)
public password2: string;
}
Password dto (password.dto.ts):
import { IsString, Length } from 'class-validator';
export abstract class PasswordDto {
@IsString()
@Length(1, 40)
public password: string;
}
Login dto (login.dto.ts):
import { IsEmail, IsString, Length } from 'class-validator';
export abstract class LoginDto {
@IsString()
@IsEmail()
@Length(7, 255)
public email: string;
@IsString()
@Length(1, 40)
public password: string;
}
On the service we need to create a public method for each route.
Register method:
// ...
import { hash } from 'bcrypt';
@Injectable()
export class AuthService implements OnModuleInit {
// ...
public async registerUser({
email,
name,
password1,
password2,
}: RegisterDto): Promise<string> {
// Check if passwords match
if (password1 !== password2)
throw new BadRequestException('Passwords do not match');
email = email.toLowerCase(); // so its always consistent and lowercase.
const count = await this.usersRepository
.search()
.where('email')
.equals(email)
.count();
// We use the count to check if the email is already in use.
if (count > 0) throw new BadRequestException('Email already in use');
// Create the user with a hashed password
const user = await this.usersRepository.createAndSave({
email,
name: name // Capitalize and trim the name
.trim()
.replace(/\n/g, ' ')
.replace(/\s\s+/g, ' ')
.replace(/\w\S*/g, (w) => w.replace(/^\w/, (l) => l.toUpperCase())),
password: await hash(password1, 10),
createdAd: new Date(),
});
return this.generateToken(user); // Generate an access token for the user
}
// ...
}
Login method:
// ...
import { hash, compare } from 'bcrypt';
@Injectable()
export class AuthService implements OnModuleInit {
// ...
public async login({ email, password }: LoginDto): Promise<string> {
// Find the first user with a given email
const user = await this.usersRepository
.search()
.where('email')
.equals(email.toLowerCase())
.first();
// Check if the user exists and the password is valid
if (!user || (await compare(password, user.password)))
throw new UnauthorizedException('Invalid credentials');
return this.generateToken(user); // Generate an access token for the user
}
// ...
}
Find by ID method:
// ...
@Injectable()
export class AuthService implements OnModuleInit {
// ...
public async userById(id: string): Promise<User> {
const user = await this.usersRepository.fetch(id);
if (!user || !user.email) throw new NotFoundException('User not found');
return user;
}
// ...
}
Remove method:
// ...
@Injectable()
export class AuthService implements OnModuleInit {
// ...
public async remove(id: string, password: string): Promise<string> {
const user = await this.userById(id);
if (!(await compare(password, user.password)))
throw new BadRequestException('Invalid password');
await this.usersRepository.remove(id);
return 'User deleted successfully';
}
// ...
}
Authorization Logic:
To be able to deal with authorization we need to create an authentication strategy, for that we're going to use passport and passport-jwt so we need to install them:
~ yarn add @nestjs/passport passport passport-jwt
~ yarn add -D @types/passport-jwt
And then create the strategy, on the auth folder create a file called jwt.strategy.ts based on the docs:
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy, VerifiedCallback } from 'passport-jwt';
import { AuthService } from './auth.service';
import { IAccessIdResponse } from './interfaces/access-id.interface';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
private readonly configService: ConfigService,
private readonly authService: AuthService,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: configService.get<string>('jwt.secret'),
ignoreExpiration: false,
passReqToCallback: false,
});
}
public async validate(
{ id, iat }: IAccessIdResponse,
done: VerifiedCallback,
) {
const user = await this.authService.userById(id);
return done(null, user.entityId, iat);
}
}
To protect our routes we need a guard, as well as a current user decorator to get the user ID, so we need to create both of them.
To generate a guard use the following commands:
~ cd src/auth
~ nest g gu jwt-auth --no-spec
~ cd ../..
On the JwtAuthGuard extend it with the Passport AuthGuard:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}
For the guard to be global and to get the user ID, we need to create the decorators, so start by creating a decorators sub-folder with two files: public.decorator.ts and current-user.decorator.ts.
Public Decorator (sets meta data for public routes):
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Current user (gets the current user id):
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
export const CurrentUser = createParamDecorator(
(_, context: ExecutionContext): string | undefined => {
return context.switchToHttp().getRequest().user;
},
);
We update both the auth module and app module for the new changes in our application.
In the auth module we need to import the PassportModule and add our jwt strategy as a provider:
// ...
import { PassportModule } from '@nestjs/passport';
import { JwtStrategy } from './jwt.strategy';
@Module({
imports: [PassportModule.register({ defaultStrategy: 'jwt' })],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}
While in the app module we need to add the auth guard:
//...
import { APP_GUARD } from '@nestjs/core';
import { JwtAuthGuard } from './auth/jwt-auth.guard';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema,
load: [config],
}),
RedisClientModule,
AuthModule,
],
providers: [AppService, { provide: APP_GUARD, useClass: JwtAuthGuard }],
controllers: [AppController],
})
export class AppModule {}
Finally to be able to use the public decorator properly we need to change the guard a little as seen in the docs:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from './decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate {
constructor(private readonly reflector: Reflector) {
super();
}
public canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
return isPublic || super.canActivate(context);
}
}
The Controller:
For the controller we just need to add the routes, but before that its good practice to create interfaces with the return value, so create the following interfaces on the interfaces sub-folder:
Access Token (access-token.interface.ts):
export interface IAccessToken {
token: string;
}
export interface IAccessIdResponse extends IAccessId {
iat: number;
exp: number;
}
Message (message.interface.ts):
export interface IMessage {
message: string;
}
User Response (user-response.interface.ts):
export interface IUserResponse {
id: string;
name: string;
email: string;
createdAt: Date;
}
Now you can add all routes:
import { Body, Controller, Delete, Get, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { CurrentUser } from './decorators/current-user.decorator';
import { Public } from './decorators/public.decorator';
import { LoginDto } from './dtos/login.dto';
import { PasswordDto } from './dtos/password.dto';
import { RegisterDto } from './dtos/register.dto';
import { IAccessToken } from './interfaces/access-token.interface';
import { IMessage } from './interfaces/message.interface';
import { IUserResponse } from './interfaces/user-response.interface';
@Controller('api/auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Public()
@Post('register')
public async register(@Body() dto: RegisterDto): Promise<IAccessToken> {
return {
token: await this.authService.register(dto),
};
}
@Public()
@Post('login')
public async login(@Body() dto: LoginDto): Promise<IAccessToken> {
return {
token: await this.authService.login(dto),
};
}
@Delete('account')
public async deleteAccount(
@CurrentUser() userId: string,
@Body() dto: PasswordDto,
): Promise<IMessage> {
return {
message: await this.authService.remove(userId, dto.password),
};
}
@Get('account')
public async findAccount(
@CurrentUser() userId: string,
): Promise<IUserResponse> {
const { name, email, entityId, createdAt } =
await this.authService.userById(userId);
return {
name,
email,
createdAt,
id: entityId,
};
}
}
Todos CRUD
The Todos Service
Again, start by using the cli to generate a REST Api resource for todos:
~ nest g res todos
But, when using this command create the CRUD enpoints.
This command will create a folder for dtos and entities, so on the on entity folder build the Todo Entity:
import { Entity, Schema } from 'redis-om';
export class Todo extends Entity {
body: string;
completed: boolean;
createdAt: Date;
author: string;
}
export const todoSchema = new Schema(Todo, {
body: { type: 'string' },
completed: { type: 'boolean' },
createdAt: { type: 'date' },
author: { type: 'string' },
});
On the dtos modify both dtos as following:
Create Todo Dto (create-todo.dto.ts):
import { IsString, Length } from 'class-validator';
export class CreateTodoDto {
@IsString()
@Length(1, 300)
public body: string;
}
Update Todo Dto (update-todo.dto.ts):
import { IsIn, IsOptional, IsString, Length } from 'class-validator';
export class UpdateTodoDto {
@IsString()
@Length(1, 300)
@IsOptional()
public body?: string;
@IsString()
@IsIn(['true', 'false', 'True', 'False'])
@IsOptional()
public completed?: string;
}
On the service we'll have the initial set up, and now we just need to inject the redis client and update all function.
First import the redis client:
import { Injectable, NotFoundException, OnModuleInit } from '@nestjs/common';
import { Repository } from 'redis-om';
import { RedisClientService } from '../redis-client/redis-client.service';
import { Todo, todoSchema } from './entities/todo.entity';
@Injectable()
export class TodosService implements OnModuleInit {
private readonly todosRepository: Repository<Todo>;
constructor(private readonly redisClient: RedisClientService) {
this.todosRepository = redisClient.fetchRepository(todoSchema);
// (async () => {await this.todosRepository.createIndex()})()
}
// ...
public async onModuleInit() {
// This could go to the constructor but I prefer it this way
await this.todosRepository.createIndex();
}
}
And then start modifying the generated methods.
Create method:
// ...
import { CreateTodoDto } from './dto/create-todo.dto';
@Injectable()
export class TodosService implements OnModuleInit {
// ...
public async create(userId: string, { body }: CreateTodoDto): Promise<Todo> {
const todo = await this.todosRepository.createAndSave({
body,
completed: false,
createdAt: new Date(),
author: userId,
});
return todo;
}
// ...
}
Update method:
// ...
import { UpdateTodoDto } from './dto/update-todo.dto';
@Injectable()
export class TodosService implements OnModuleInit {
// ...
public async update(
userId: string,
todoId: string,
{ body, completed }: UpdateTodoDto,
): Promise<Todo> {
const todo = await this.findOne(userId, todoId);
if (body && todo.body !== body) todo.body = body;
if (completed) {
const boolComplete = completed.toLowerCase() === 'true';
if (todo.completed !== boolComplete) todo.completed = boolComplete;
}
await this.todosRepository.save(todo);
return todo;
}
// ...
}
Find All method (completed is a query paramenter):
@Injectable()
export class TodosService implements OnModuleInit {
// ...
public async findAll(userId: string, completed?: boolean): Promise<Todo[]> {
const qb = this.todosRepository.search().where('author').equals(userId);
if (completed !== null) {
qb.where('completed').equals(completed);
}
return qb.all();
}
// ...
}
Find One method:
@Injectable()
export class TodosService implements OnModuleInit {
// ...
public async findOne(userId: string, todoId: string): Promise<Todo> {
const todo = await this.todosRepository.fetch(todoId);
if (!todo || todo.author !== userId)
throw new NotFoundException('Todo not found');
return todo;
}
// ...
}
Remove method:
@Injectable()
export class TodosService implements OnModuleInit {
// ...
public async remove(userId: string, todoId: string): Promise<string> {
const todo = await this.findOne(userId, todoId);
await this.todosRepository.remove(todo.entityId);
return 'Todo removed successfully';
}
// ...
}
The Todos Controller
The todos controller is just a basic controller with all the methods given by the generated controller but with "api/todos" as the main route.
import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
Query,
} from '@nestjs/common';
import { CurrentUser } from '../auth/decorators/current-user.decorator';
import { CreateTodoDto } from './dto/create-todo.dto';
import { UpdateTodoDto } from './dto/update-todo.dto';
import { TodosService } from './todos.service';
@Controller('api/todos')
export class TodosController {
constructor(private readonly todosService: TodosService) {}
@Post()
public async create(
@CurrentUser() userId: string,
@Body() dto: CreateTodoDto,
) {
return this.todosService.create(userId, dto);
}
@Get()
public async findAll(
@CurrentUser() userId: string,
@Query('completed') completed?: string,
) {
if (completed) {
completed = completed.toLowerCase();
if (completed !== 'true' && completed !== 'false')
throw new BadRequestException('Invalid completed query parameter');
}
return this.todosService.findAll(
userId,
completed ? completed === 'true' : null,
);
}
@Get(':id')
public async findOne(@CurrentUser() userId: string, @Param('id') id: string) {
return this.todosService.findOne(userId, id);
}
@Patch(':id')
public async update(
@CurrentUser() userId: string,
@Param('id') id: string,
@Body() dto: UpdateTodoDto,
) {
return this.todosService.update(userId, id, dto);
}
@Delete(':id')
public async remove(@CurrentUser() userId: string, @Param('id') id: string) {
return this.todosService.remove(userId, id);
}
}
Puting it all together
To be able to run the app in development we still need to update the main file, to add both the port from the configuration and set up a global validation pipe for class-validator:
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const configService = app.get(ConfigService);
app.useGlobalPipes(new ValidationPipe());
await app.listen(configService.get<number>('port'));
}
bootstrap();
The full working project can be found here.
Dynamic Module
Now if you use a nestjs monorepo, you'll probably need a redis-client library, this is where dynamic modules are necessary, as you'll need a client library for all your apps.
After generating the redis-client library with the nestjs cli:
~ nest g lib redis-client --no-spec
Start by creating a interfaces folder with three files (including the index.ts):
Redis Options (redis-options.interface.ts), the options for the register static method:
export interface IRedisOptions {
url: string;
}
Redis Async Options (redis-async-options.interface.ts), the options for the register async static method:
import { ModuleMetadata, Type } from '@nestjs/common';
import { IRedisOptions } from './redis-options.interface';
export interface IRedisOptionsFactory {
createRedisOptions(): Promise<IRedisOptions> | IRedisOptions;
}
export interface IRedisAsyncOptions extends Pick<ModuleMetadata, 'imports'> {
useFactory?: (...args: any[]) => Promise<IRedisOptions> | IRedisOptions;
useClass?: Type<IRedisOptionsFactory>;
inject?: any[];
}
On the index.ts just export the other types of the other files:
export type { IRedisOptions } from './redis-options.interface';
export type {
IRedisOptionsFactory,
IRedisAsyncOptions,
} from './redis-async-options.interface';
The only difference between the past version of the redis-client module and this one is that now we have static function to register the client on.
So to inject options on dynamic modules we need a constants.ts that exports a string constant:
export const REDIS_OPTIONS = 'REDIS_OPTIONS';
In the end the module will look something like this, I won't touch on the logic of each method that much as they're very basic and follow the community guidelines.
import { DynamicModule, Global, Module, Provider } from '@nestjs/common';
import { RedisClientService } from './redis-client.service';
import {
IRedisAsyncOptions,
IRedisOptions,
IRedisOptionsFactory,
} from './interfaces';
import { REDIS_OPTIONS } from './constants';
@Global()
@Module({
providers: [RedisClientService],
exports: [RedisClientService],
})
export class RedisClientModule {
public static forRoot(options: IRedisOptions): DynamicModule {
return {
module: RedisOrmModule,
global: true,
providers: [
{
provide: REDIS_OPTIONS,
useValue: options,
},
],
};
}
public static forRootAsync(options: IRedisAsyncOptions): DynamicModule {
return {
module: RedisOrmModule,
imports: options.imports,
providers: this.createAsyncProviders(options),
};
}
private static createAsyncProviders(options: IRedisAsyncOptions): Provider[] {
const providers: Provider[] = [this.createAsyncOptionsProvider(options)];
if (options.useClass) {
providers.push({
provide: options.useClass,
useClass: options.useClass,
});
}
return providers;
}
private static createAsyncOptionsProvider(
options: IRedisAsyncOptions,
): Provider {
if (options.useFactory) {
return {
provide: REDIS_OPTIONS,
useFactory: options.useFactory,
inject: options.inject || [],
};
}
return {
provide: REDIS_OPTIONS,
useFactory: async (optionsFactory: IRedisOptionsFactory) =>
await optionsFactory.createRedisOptions(),
inject: options.useClass ? [options.useClass] : [],
};
}
}
The service is pretty identical to the normal one but url comes from the injected options and not the config service:
import { Inject, Injectable, OnModuleDestroy } from '@nestjs/common';
import { Client } from 'redis-om';
import { REDIS_OPTIONS } from './constants';
import { IRedisOptions } from './interfaces';
@Injectable()
export class RedisClientService extends Client implements OnModuleDestroy {
constructor(@Inject(REDIS_OPTIONS) options: IRedisOptions) {
super();
(async () => {
await this.open(options.url);
})();
}
public async onModuleDestroy() {
await this.close();
}
}
Thanks for reading my article to the end, and good luck on the redis hackathon.
Top comments (3)
Hey @tugascript - This is great !
My first time using Redis + I was running into issues following official docs using Nest.
I'm running into a problem with a fairly vague error message following your tutorial - any ideas? :
Read more here: docs.nestjs.com/techniques/caching
This is not Redis, this is for RedisJSON with RedisOM, if you just want normal redis you are better of using the nestjs cache-manager with ioredis.