2026年版 NestJSとRedis:キャッシング、セッション管理、面接対策

NestJSでRedisを活用したキャッシングとセッション管理の実装方法を解説します。@nestjs/cache-managerの設定、キャッシュ戦略、セッション永続化、Node.jsキャッシングに関する技術面接の頻出質問を網羅的に紹介します。

NestJSとRedisキャッシングアーキテクチャ図:セッション管理とキャッシュレイヤーを示す

NestJSとRedisの統合により、データベースへのアクセスをインメモリ検索に置き換えることで、アプリケーションのパフォーマンスが劇的に向上します。キャッシング用の@nestjs/cache-managerとセッション永続化用のexpress-session + connect-redisの組み合わせは、バックエンドアプリケーションにおける最も一般的なRedisユースケースをカバーしています。

重要ポイント

NestJSでのRedisキャッシングは、頻繁にアクセスされるデータをメモリに保存することでデータベース負荷を軽減します。Redisによるセッション管理は、複数インスタンス間でセッション状態を共有することで水平スケーリングを実現します。

@nestjs/cache-managerとRedisのセットアップ

NestJS 10以降、cache-managerモジュールは独立したパッケージに移行しました。インストールにはNestJSラッパーと基盤となるキャッシュストアの両方が必要です。cache-manager v5でのRedis統合には@keyv/redisアダプターが推奨されています。

bash
# Install required packages
npm install @nestjs/cache-manager cache-manager @keyv/redis

モジュール登録でRedis接続とデフォルトTTLを設定します。isGlobalオプションを使用すると、再インポートなしですべてのモジュールでキャッシュが利用可能になります。

app.module.tstypescript
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import KeyvRedis from '@keyv/redis';

@Module({
  imports: [
    CacheModule.registerAsync({
      isGlobal: true,
      useFactory: () => ({
        stores: [
          new KeyvRedis(process.env.REDIS_URL || 'redis://localhost:6379'),
        ],
        ttl: 60000, // Default TTL in milliseconds
      }),
    }),
  ],
})
export class AppModule {}

stores配列は多層キャッシング用に複数のキャッシュストアを受け入れます。本番環境では通常、Redisをプライマリストアとして使用し、開発用にオプションのインメモリフォールバックを設定します。

キャッシュインジェクションとサービスレベルキャッシング

CACHE_MANAGERトークンは、サービスレベルの操作でキャッシュに直接アクセスする手段を提供します。このアプローチは、ビジネスロジックがキャッシュの無効化を決定する場合に、自動HTTPキャッシングよりも細かい制御が可能です。

users.service.tstypescript
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { UsersRepository } from './users.repository';
import { User } from './user.entity';

@Injectable()
export class UsersService {
  constructor(
    @Inject(CACHE_MANAGER) private cache: Cache,
    private usersRepository: UsersRepository,
  ) {}

  async findById(id: string): Promise<User | null> {
    // Check cache first
    const cacheKey = `user:${id}`;
    const cached = await this.cache.get<User>(cacheKey);
    
    if (cached) {
      return cached;
    }

    // Cache miss: fetch from database
    const user = await this.usersRepository.findById(id);
    
    if (user) {
      await this.cache.set(cacheKey, user, 300000); // 5 minutes TTL
    }

    return user;
  }

  async update(id: string, data: Partial<User>): Promise<User> {
    const user = await this.usersRepository.update(id, data);
    
    // Invalidate cache on update
    await this.cache.del(`user:${id}`);
    
    return user;
  }
}

user:{id}というキャッシュキーパターンにより、ターゲットを絞った無効化が可能です。user:*のようなより複雑なパターンでは、基盤となるクライアントを通じてRedis固有のコマンドが必要になります。

CacheInterceptorによる自動HTTPレスポンスキャッシング

組み込みのCacheInterceptorは、HTTP GETレスポンスを自動的にキャッシュします。コントローラーレベルで適用するとすべてのGETエンドポイントがキャッシュされ、メソッドレベルで適用すると細かい制御が可能です。

products.controller.tstypescript
import { Controller, Get, UseInterceptors, Param } from '@nestjs/common';
import { CacheInterceptor, CacheTTL, CacheKey } from '@nestjs/cache-manager';
import { ProductsService } from './products.service';

@Controller('products')
@UseInterceptors(CacheInterceptor)
export class ProductsController {
  constructor(private productsService: ProductsService) {}

  @Get()
  @CacheTTL(120000) // Override default TTL: 2 minutes
  async findAll() {
    return this.productsService.findAll();
  }

  @Get(':id')
  @CacheKey('product-detail') // Custom cache key prefix
  async findOne(@Param('id') id: string) {
    return this.productsService.findById(id);
  }
}

インターセプターはデフォルトでリクエストURLからキャッシュキーを生成します。クエリパラメータがキャッシングに影響しないルートでは、カスタム@CacheKey()デコレーターでこの動作をオーバーライドします。NestJSのインターセプターとガードについては、技術面接で頻繁に取り上げられる概念です。

キャッシュキーの衝突

デフォルトのキャッシュキーはクエリパラメータを含む完全なURLを使用します。/products?page=1/products?page=2への2つのリクエストは別々のキャッシュエントリを作成します。ページネーションで重複したキャッシュレスポンスを作成したくない場合は、@CacheKey()でオーバーライドしてください。

Redisとexpress-sessionによるセッション管理

NestJSはデフォルトでExpress上で動作するため、express-sessionがセッション管理の標準的な選択肢です。connect-redisアダプターはセッションデータをメモリではなくRedisに保存し、サーバー再起動後もセッションを永続化します。

bash
# Install session packages
npm install express-session connect-redis redis
npm install -D @types/express-session

セッション設定はmain.tsでアプリケーション起動前に行います。RedisクライアントはCacheManagerのセットアップとは独立して接続します。

main.tstypescript
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as session from 'express-session';
import { createClient } from 'redis';
import RedisStore from 'connect-redis';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Initialize Redis client
  const redisClient = createClient({
    url: process.env.REDIS_URL || 'redis://localhost:6379',
  });
  await redisClient.connect();

  // Configure session middleware
  app.use(
    session({
      store: new RedisStore({ client: redisClient }),
      secret: process.env.SESSION_SECRET || 'change-this-in-production',
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        maxAge: 24 * 60 * 60 * 1000, // 24 hours
      },
    }),
  );

  await app.listen(3000);
}
bootstrap();

saveUninitialized: falseオプションは空のセッションの保存を防ぎ、Redisのメモリ使用量を削減します。本番環境ではsecure: trueのCookie設定にHTTPSが必要です。

Node.js / NestJSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

コントローラーとガードでのセッションアクセス

セッションデータはリクエストオブジェクトを通じてアクセス可能です。型付きセッションインターフェースにより、開発者体験が向上し、コンパイル時にエラーを検出できます。

session.interface.tstypescript
export interface SessionData {
  userId?: string;
  email?: string;
  roles?: string[];
  loginAt?: Date;
}

declare module 'express-session' {
  interface SessionData {
    userId?: string;
    email?: string;
    roles?: string[];
    loginAt?: Date;
  }
}

拡張されたセッション型により、オートコンプリートでセッションプロパティが利用可能になります。

auth.controller.tstypescript
import { Controller, Post, Body, Req, HttpCode } from '@nestjs/common';
import { Request } from 'express';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  @HttpCode(200)
  async login(@Body() loginDto: LoginDto, @Req() req: Request) {
    const user = await this.authService.validateUser(
      loginDto.email,
      loginDto.password,
    );

    // Store user data in session
    req.session.userId = user.id;
    req.session.email = user.email;
    req.session.roles = user.roles;
    req.session.loginAt = new Date();

    return { message: 'Logged in successfully' };
  }

  @Post('logout')
  @HttpCode(200)
  async logout(@Req() req: Request) {
    return new Promise((resolve, reject) => {
      req.session.destroy((err) => {
        if (err) reject(err);
        resolve({ message: 'Logged out successfully' });
      });
    });
  }
}

ガードはセッションの有効性をチェックしてルートを保護します。このパターンはNestJSの認可とRBACシステムとシームレスに統合できます。

session.guard.tstypescript
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

@Injectable()
export class SessionGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    return !!request.session?.userId;
  }
}

NestJSアプリケーションのキャッシュ無効化戦略

キャッシュ無効化はキャッシングにおいてより難しい問題です。異なるシナリオに適用される3つの戦略があります。

時間ベースの有効期限(TTL) は、データの変更が予測可能な場合や、わずかな古さが許容される場合に有効です。商品カタログ、設定値、公開コンテンツがこのパターンに適しています。

イベントベースの無効化 は、基礎となるデータが変更されたときにキャッシュエントリをクリアします。これには更新および削除操作で明示的なcache.del()呼び出しが必要です。

パターンベースの無効化 は、関連する複数のキーを削除します。RedisはSCANDELコマンドでパターン削除をサポートしていますが、cache-managerはこれを抽象化しています。

cache-invalidation.service.tstypescript
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';

@Injectable()
export class CacheInvalidationService {
  constructor(@Inject(CACHE_MANAGER) private cache: Cache) {}

  // Single key invalidation
  async invalidateUser(userId: string): Promise<void> {
    await this.cache.del(`user:${userId}`);
  }

  // Multiple related keys
  async invalidateUserRelated(userId: string): Promise<void> {
    const keys = [
      `user:${userId}`,
      `user:${userId}:profile`,
      `user:${userId}:preferences`,
    ];
    
    await Promise.all(keys.map((key) => this.cache.del(key)));
  }

  // Clear entire cache (use sparingly)
  async clearAll(): Promise<void> {
    await this.cache.reset();
  }
}

reset()メソッドはすべてのキャッシュデータをクリアします。デプロイやデータマイグレーション時には有用ですが、本番環境で誤って呼び出すと危険です。

Redis接続プーリングとクラスターサポート

本番環境へのデプロイでは、同時リクエストを効率的に処理するために接続プーリングが必要です。ioredisライブラリは、クラスターサポートと接続プーリングをすぐに利用できる形で提供しています。

redis.config.tstypescript
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import Redis, { Cluster } from 'ioredis';

@Injectable()
export class RedisConfig {
  constructor(private configService: ConfigService) {}

  createClient(): Redis | Cluster {
    const clusterNodes = this.configService.get<string>('REDIS_CLUSTER_NODES');

    if (clusterNodes) {
      // Cluster mode
      const nodes = clusterNodes.split(',').map((node) => {
        const [host, port] = node.split(':');
        return { host, port: parseInt(port, 10) };
      });

      return new Redis.Cluster(nodes, {
        redisOptions: {
          password: this.configService.get('REDIS_PASSWORD'),
        },
      });
    }

    // Standalone mode
    return new Redis({
      host: this.configService.get('REDIS_HOST', 'localhost'),
      port: this.configService.get('REDIS_PORT', 6379),
      password: this.configService.get('REDIS_PASSWORD'),
      maxRetriesPerRequest: 3,
    });
  }
}

TypeORMやPrismaを使用するアプリケーションでは、データベースとキャッシュの接続をそれぞれ独自の接続プールで個別に設定する必要があります。

NestJS Redis面接質問集

Node.jsキャッシングに関する技術面接では、概念の理解と実践的な実装の両方がカバーされます。以下の質問が頻繁に出題されます。

インメモリキャッシングではなくRedisを使用する理由は?

インメモリキャッシングは再起動後に永続化されず、複数のインスタンス間で共有できません。Redisは水平スケーリングされたアプリケーション向けに、永続化、レプリケーション、共有状態を提供します。トレードオフは、ローカルメモリアクセスと比較したネットワークレイテンシです。

cache-managerと直接Redis使用の違いは?

cache-managerライブラリは異なるキャッシュストア間で統一されたAPIを提供します。インメモリからRedisへの切り替えは、サービスコードを変更せずにストア設定を変更するだけで可能です。直接Redis使用では、pub/sub、ソート済みセット、Luaスクリプティングなど、より多くのRedis固有の機能が利用可能です。

キャッシュスタンピードとは何か、どのように防止するか?

キャッシュスタンピードは、人気のあるキャッシュエントリが期限切れになり、多数の同時リクエストが同時にデータベースにアクセスする際に発生します。防止策として以下があります:

  • ロックベースのリフレッシュ:1つのリクエストがキャッシュをリフレッシュし、他は待機
  • 確率的早期有効期限:TTL期限前にランダムにリフレッシュ
  • バックグラウンドリフレッシュ:別プロセスが期限切れ間近のエントリをリフレッシュ

CacheInterceptorとサービスレベルキャッシングはいつ使い分けるべきか?

CacheInterceptorは、レスポンスがURLのみに依存するステートレスなGETエンドポイントに適しています。サービスレベルキャッシングは、データ変更時にキャッシュ無効化が必要なシナリオや、非GET操作でキャッシングが必要な場合に対応します。

Redisセッションでセッション固定攻撃をどのように防ぐか?

認証後にセッションIDを再生成します。攻撃者が認証前のセッションIDを使用することを防ぐため、ユーザー資格情報を保存する前にreq.session.regenerate()を呼び出します。

typescript
// Secure login with session regeneration
async login(@Body() loginDto: LoginDto, @Req() req: Request) {
  const user = await this.authService.validateUser(
    loginDto.email,
    loginDto.password,
  );

  return new Promise((resolve, reject) => {
    req.session.regenerate((err) => {
      if (err) reject(err);
      
      req.session.userId = user.id;
      req.session.email = user.email;
      resolve({ message: 'Logged in successfully' });
    });
  });
}
面接のヒント

シニア候補者はキャッシング戦略間のトレードオフを説明します。ジュニアは「キャッシングはパフォーマンスを向上させる」と言うかもしれません。シニアは、高度にパーソナライズされたデータや急速に変化するデータなど、キャッシングが利益なしに複雑さを追加するケースを説明します。

パフォーマンス監視とRedisメトリクス

本番アプリケーションにはキャッシュパフォーマンスの可視性が必要です。主要なメトリクスには以下があります:

  • ヒット率:キャッシュから提供されたリクエストの割合。80%未満はTTLまたはキー戦略の問題を示唆します。
  • メモリ使用量:Redis INFO memoryコマンドで現在およびピークメモリを表示します。
  • 接続数:接続数が多すぎる場合はプーリングが不足しています。
  • エビクション数:非ゼロのエビクションはキャッシュが満杯でエントリを削除していることを意味します。
redis-health.service.tstypescript
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisHealthService {
  constructor(private redis: Redis) {}

  async getMetrics() {
    const info = await this.redis.info('stats');
    const memory = await this.redis.info('memory');
    
    const stats = this.parseInfo(info);
    const mem = this.parseInfo(memory);

    const hits = parseInt(stats.keyspace_hits, 10);
    const misses = parseInt(stats.keyspace_misses, 10);
    const hitRate = hits / (hits + misses) || 0;

    return {
      hitRate: (hitRate * 100).toFixed(2) + '%',
      usedMemory: mem.used_memory_human,
      connectedClients: stats.connected_clients,
      evictedKeys: stats.evicted_keys,
    };
  }

  private parseInfo(info: string): Record<string, string> {
    return info.split('\n').reduce((acc, line) => {
      const [key, value] = line.split(':');
      if (key && value) acc[key.trim()] = value.trim();
      return acc;
    }, {} as Record<string, string>);
  }
}

これらのメトリクスをヘルスチェックエンドポイントで公開するか、Prometheusと統合して本番監視を行います。

本番NestJSデプロイにおけるRedisキャッシングとセッション

NestJSをRedisとともにデプロイする際には、いくつかの運用上の考慮事項に注意が必要です:

  • 接続文字列の管理:Redis URLはコードではなく環境変数に保存します。パスワードにはシークレット管理を使用します。
  • グレースフルシャットダウン:アプリケーションシャットダウン時にRedis接続を閉じて接続リークを防ぎます。
  • リトライロジック:Redis接続障害に対して指数バックオフを設定します。
  • 別々のRedisインスタンス:キャッシュとセッション用に別々のRedisインスタンスを検討します。キャッシュのエビクションがセッションデータに影響しないようにします。
graceful-shutdown.tstypescript
import { Injectable, OnModuleDestroy } from '@nestjs/common';
import Redis from 'ioredis';

@Injectable()
export class RedisCleanup implements OnModuleDestroy {
  constructor(private redis: Redis) {}

  async onModuleDestroy() {
    await this.redis.quit();
  }
}

マイクロサービスアーキテクチャでは、NestJSの組み込みトランスポートレイヤーを通じて、Redisはメッセージブローカーとしても機能し、キャッシング、セッション、サービス間通信を統合します。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

今日のチャレンジ

Node.js / NestJS のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年9月10日 更新

タグ

#nestjs
#redis
#caching
#nodejs
#sessions
#interview

共有

関連記事