Tutoriel : Caching avec NestJS et Redis
Pourquoi Caching avec NestJS et Redis ?
Contexte réel : En tant que développeur backend, vous avez probablement déjà rencontré des problèmes de performance liés à la récupération de données complexes ou à des opérations intensives. Le caching est une technique efficace pour améliorer ces performances en stockant les résultats d'opérations coûteuses dans une mémoire temporaire, permettant ainsi aux futures requêtes identiques de satisfaire rapidement cette mémoire plutôt que de recalculer ou de récupérer les données à nouveau.
Un cas d'utilisation concret : Imaginez une application e-commerce qui affiche des produits en fonction du panier d'un utilisateur. Chaque fois qu'un utilisateur ajoute un produit au panier, la page du panier doit être recalculée pour refléter le changement. Si vous n'utilisez pas de caching, chaque fois que l'utilisateur accède à sa page de panier, votre application devra effectuer des opérations coûteuses comme récupérer les informations sur tous les produits dans son panier depuis la base de données. Avec le caching, vous pouvez stocker ces informations en mémoire pour une certaine durée, évitant ainsi les opérations coûteuses et réduisant considérablement le temps de réponse.
Prérequis
Connaissances nécessaires :
- Connaissance avancée de NestJS
- Expérience avec Node.js
- Familiarité avec Redis
Outils à installer (versions) :
- Node.js >= 14.0.0
- npm >= 6.0.0
- Redis >= 5.0.0
Concepts fondamentaux
1. Caching en général
Le caching est une technique consistant à stocker les résultats d'opérations dans une mémoire temporaire pour des accès rapides futurs. Dans le contexte de NestJS, nous utiliserons Redis comme base de données mémoire.
Schéma mental :
- Opération coûteuse : L'action qui est trop lente à exécuter sans optimisation.
- Caching : Stockage temporaire des résultats de ces opérations pour un accès rapide futur.
- Expiration : Durée durant laquelle les données sont stockées en cache avant d'être supprimées.
Code fonctionnel :
// Importer le module Redis
import * as redis from 'redis';
// Créer une instance de client Redis
const client = redis.createClient();
client.on('error', (err) => {
console.log('Redis error:', err);
});
client.on('connect', () => {
console.log('Connected to Redis');
});
2. Intégration avec NestJS
NestJS fournit un module de base pour intégrer le caching, mais nous utiliserons Redis directement.
Schéma mental :
- Module Cache : Fournit des services pour gérer le caching.
- Middleware Caching : Permet d'ajouter du caching à vos contrôleurs NestJS.
Code fonctionnel :
// Importer les modules nécessaires
import { Module, MiddlewareConsumer } from '@nestjs/common';
import * as redis from 'redis';
@Module({
providers: [
{
provide: 'REDIS_CLIENT',
useFactory: () => redis.createClient(),
},
],
})
export class CacheModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(CacheMiddleware)
.forRoutes('*');
}
}
3. Utilisation du caching
Nous allons utiliser le caching pour stocker les résultats d'une requête à la base de données.
Schéma mental :
- Récupération des données : L'action qui récupère les données.
- Stockage en cache : Stocke les données récupérées dans Redis.
- Récupération depuis le cache : Récupère les données stockées en cache si elles existent.
Code fonctionnel :
// Importer les modules nécessaires
import { Injectable } from '@nestjs/common';
import * as redis from 'redis';
@Injectable()
export class ProductService {
private client: redis.RedisClient;
constructor() {
this.client = redis.createClient();
}
async getProductById(id: string): Promise<any> {
const cachedProduct = await new Promise((resolve, reject) => {
this.client.get(`product:${id}`, (err, reply) => {
if (err) return reject(err);
resolve(reply ? JSON.parse(reply) : null);
});
});
if (cachedProduct) {
console.log('Retrieved from cache');
return cachedProduct;
}
const product = await this.fetchProductFromDatabase(id); // Simulate database fetch
this.client.setex(`product:${id}`, 3600, JSON.stringify(product)); // Cache for 1 hour
return product;
}
private async fetchProductFromDatabase(id: string): Promise<any> {
// Simulate database fetch
console.log('Fetching from database');
return { id, name: 'Example Product', price: 19.99 };
}
}
Mise en pratique : projet fil rouge
Projet fil rouge : Nous allons créer un simple API de blog qui utilise le caching pour stocker les articles.
Structure des fichiers :
src/
├── app.controller.ts
├── app.module.ts
├── blog/
│ ├── blog.controller.ts
│ ├── blog.service.ts
│ └── blog.entity.ts
└── cache/
└── cache.middleware.ts
1. Configuration de l'application
app.module.ts :
import { Module } from '@nestjs/common';
import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
import { CacheModule } from './cache/cache.module';
@Module({
imports: [
CacheModule,
// Other modules
],
controllers: [AppController],
providers: [
// Services
],
})
export class AppModule {}
2. Middleware de caching
cache.middleware.ts :
import { Injectable, NestMiddleware } from '@nestjs/common';
import * as redis from 'redis';
@Injectable()
export class CacheMiddleware implements NestMiddleware {
private client: redis.RedisClient;
constructor() {
this.client = redis.createClient();
}
use(req: any, res: any, next: () => void) {
const key = req.url;
this.client.get(key, (err, reply) => {
if (err) {
console.error('Redis error:', err);
return next();
}
if (reply !== null) {
// Return cached response
res.send(JSON.parse(reply));
} else {
// Proceed with the request and store the result in cache
next();
const originalSend = res.send;
res.send = (body: any) => {
this.client.setex(key, 3600, JSON.stringify(body)); // Cache for 1 hour
originalSend.call(res, body);
};
}
});
}
}
3. Service du blog
blog.service.ts :
import { Injectable } from '@nestjs/common';
import * as redis from 'redis';
@Injectable()
export class BlogService {
private client: redis.RedisClient;
constructor() {
this.client = redis.createClient();
}
async getPosts(): Promise<any[]> {
const cachedPosts = await new Promise((resolve, reject) => {
this.client.get('posts', (err, reply) => {
if (err) return reject(err);
resolve(reply ? JSON.parse(reply) : null);
});
});
if (cachedPosts) {
console.log('Retrieved from cache');
return cachedPosts;
}
const posts = await this.fetchPostsFromDatabase(); // Simulate database fetch
this.client.setex('posts', 3600, JSON.stringify(posts)); // Cache for 1 hour
return posts;
}
private async fetchPostsFromDatabase(): Promise<any[]> {
// Simulate database fetch
console.log('Fetching from database');
return [
{ id: 1, title: 'First Post', content: 'This is the first post.' },
{ id: 2, title: 'Second Post', content: 'This is the second post.' },
];
}
}
4. Contrôleur du blog
blog.controller.ts :
import { Controller, Get } from '@nestjs/common';
import { BlogService } from './blog.service';
@Controller('blog')
export class BlogController {
constructor(private readonly blogService: BlogService) {}
@Get()
async getPosts() {
return this.blogService.getPosts();
}
}
5. Installation et exécution
Commandes terminal :
npm install redis
npm run start
Erreurs fréquentes et debugging
1. Connexion au Redis échoue
Code incorrect :
const client = redis.createClient();
Code correct :
import * as redis from 'redis';
const client = redis.createClient({
url: 'redis://localhost', // Ensure the URL is correct
});
2. Stockage en cache échoue
Code incorrect :
this.client.setex('posts', 3600, JSON.stringify(posts));
Code correct :
const result = await new Promise((resolve, reject) => {
this.client.setex('posts', 3600, JSON.stringify(posts), (err) => {
if (err) return reject(err);
resolve();
});
});
3. Récupération depuis le cache échoue
Code incorrect :
this.client.get('posts');
Code correct :
const result = await new Promise((resolve, reject) => {
this.client.get('posts', (err, reply) => {
if (err) return reject(err);
resolve(reply ? JSON.parse(reply) : null);
});
});
Pour aller plus loin
1. Gestion des erreurs Redis
Concept avancé :
- Retries et backoffs : Gérer les erreurs Redis en utilisant des réessais et des retards.
Code fonctionnel :
const retry = require('async-retry');
async function fetchData() {
await retry(async bail => {
const result = await new Promise((resolve, reject) => {
this.client.get('posts', (err, reply) => {
if (err) return reject(err);
resolve(reply ? JSON.parse(reply) : null);
});
});
if (!result) {
bail(new Error('Failed to fetch data from Redis'));
}
return result;
}, { retries: 3 });
// Continue with the rest of the code
}
2. Utilisation de pipelines Redis
Concept avancé :
- Pipelines : Optimiser les opérations Redis en groupant plusieurs commandes dans une seule requête.
Code fonctionnel :
const pipeline = this.client.pipeline();
pipeline.setex('posts', 3600, JSON.stringify(posts));
pipeline.get('posts');
const result = await pipeline.exec();
const [setResult, getResult] = result;
console.log('Set result:', setResult);
console.log('Get result:', getResult[1]);
3. Authentification Redis
Concept avancé :
- Authentification : S'authentifier avec un serveur Redis protégé.
Code fonctionnel :
const client = redis.createClient({
url: 'redis://username:password@localhost',
});
Défi pratique
Défi :
- Implémenter le caching pour une application de gestionnaire de tâches. Stockez les listes de tâches en mémoire et utilisez le middleware de caching pour améliorer les performances des requêtes.
Ce tutoriel vous a montré comment utiliser Redis avec NestJS pour améliorer la performance de votre application en stockant les résultats d'opérations coûteuses dans une mémoire temporaire. En suivant ces étapes, vous devriez être capable de mettre en œuvre le caching efficacement dans vos projets NestJS.