homeresume
 
   

Redis as custom storage for NestJS rate limiter

September 14, 2021

Rate limiter is a common technique to protect against brute-force attacks. NestJS provides a module for it, the default storage is in-memory. Custom storage should be injected inside ThrottlerModule configuration.

// src/modules/app.module.ts
ThrottlerModule.forRootAsync({
imports: [ThrottlerStorageModule],
useFactory: (throttlerStorage: ThrottlerStorageService) => ({
ttl,
limit,
storage: throttlerStorage,
}),
inject: [ThrottlerStorageService],
}),

Custom storage needs to implement the ThrottlerStorage interface.

// src/modules/throttler-storage/throttler-storage.service.ts
@Injectable()
export class ThrottlerStorageService implements ThrottlerStorage {
constructor(@InjectRedis() private readonly throttlerStorageService: Redis) {}
async getRecord(key: string): Promise<number[]> {
// ...
}
async addRecord(key: string, ttl: number): Promise<void> {
// ...
}
}

Redis client should be configured inside RedisModule configuration.

// src/modules/throttler-storage/throttler-storage.module.ts
@Module({
imports: [
RedisModule.forRootAsync({
useFactory: (configService: ConfigService) => {
const redisUrl = configService.get('REDIS_URL');
return {
config: {
url: redisUrl
}
};
},
inject: [ConfigService]
})
],
providers: [ThrottlerStorageService],
exports: [ThrottlerStorageService]
})
export class ThrottlerStorageModule {}

Redis connection should be closed during the graceful shutdown.

// src/app/app.module.ts
@Injectable()
export class AppModule implements OnApplicationShutdown {
constructor(
// ...
@InjectRedis() private readonly redisConnection: Redis
) {}
async closeRedisConnection(): Promise<void> {
await this.redisConnection.quit();
this.logger.log('Redis connection is closed');
}
async onApplicationShutdown(signal: string): Promise<void> {
// ...
await Promise.all([
// ...
this.closeRedisConnection()
]).catch((error) => this.logger.error(error.message));
}
}

@liaoliaots/nestjs-redis library is used in the examples.

Testing custom repositories (NestJS/TypeORM)

September 5, 2021

Custom repositories extend base repository class and enrich it with several additional methods.

// user.repository.ts
@Injectable()
@EntityRepository(UserEntity)
export class UserRepository extends Repository<UserEntity> {
async getById(id: string): Promise<User> {
return this.findOne({ id });
}
// ...
}

Custom repository can be injected into the service using the InjectRepository decorator.

export class UserService {
constructor(
@InjectRepository(UserRepository)
private readonly userRepository: UserRepository,
) {}
async getById(id: string): Promise<User> {
return this.userRepository.getById(id);
}
// ...
}

Instantiation responsibility can be delegated to Nest by passing repository class to the forFeature method.

@Module({
imports: [
TypeOrmModule.forFeature([UserRepository]),
// ...
],
providers: [UserService],
// ...
})
export class UserModule {}

In order to properly test the custom repository, some of the methods has to be mocked.

describe('UserRepository', () => {
let userRepository: UserRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserRepository],
}).compile();
userRepository = module.get<UserRepository>(UserRepository);
});
describe('getById', () => {
it('should return found user', async () => {
const id = 'id';
const user = {
id,
};
const findOneSpy = jest
.spyOn(userRepository, 'findOne')
.mockResolvedValue(user as UserEntity);
const foundUser = await userRepository.getById(id);
expect(foundUser).toEqual(user);
expect(findOneSpy).toHaveBeenCalledWith(user);
});
});
});

Server-Sent Events 101

August 18, 2021

Server-Sent Events (SSE) is a unidirectional communication between client and server where the server sends the events in text/event-stream format to the client once the client-server connection is established by the client. The client initiates the connection with the server using EventSource API. Previously mentioned API can also be used for listening to the events from the server, listening for the errors, and closing the connection.

const eventSource = new EventSource(url);
eventSource.onmessage = ({ data }) => {
const eventData = JSON.parse(data);
// handling the data from the server
};
eventSource.onerror = () => {
// error handling
};
eventSource.close();

A server can filter clients by query parameter and send them only the appropriate events. In the following example server sends the events only to a specific client distinguished by its e-mail address.

@Sse('sse')
sse(@Query() sseQuery: SseQueryDto): Observable<MessageEvent> {
const subject$ = new Subject();
this.eventService.on(FILTER_VERIFIED, data => {
if (sseQuery.email !== data.email) return;
subject$.next({ isVerifiedFilter: true });
});
return subject$.pipe(
map((data: MessageEventData): MessageEvent => ({ data })),
);
}

 

© 2021