NestJSはデフォルトでExpressを使用しますが、高負荷環境ではパフォーマンスがボトルネックになることがあります。本記事では、Fastifyアダプターへの移行による高速化、効果的なキャッシュ戦略の実装、レスポンス圧縮の設定、そしてパフォーマンス計測と監視の手法を解説します。これらのテクニックを組み合わせることで、本番環境で安定して高負荷に耐えるアプリケーションを構築できます。

前提条件

本記事の内容を実践するには、以下の環境が必要です。

項目 バージョン・条件
Node.js 20.x 以上
NestJS 11.x
TypeScript 5.x
npm または yarn 最新版
OS Windows / macOS / Linux

事前に以下の準備を完了してください。

  • NestJSプロジェクトの作成済み
  • NestJSの基本的なアーキテクチャ(Module、Controller、Provider)の理解
  • @nestjs/configによる環境変数管理の基本理解

NestJSプロジェクトの作成方法はNestJS入門記事を、環境変数管理は@nestjs/configによる環境設定管理を参照してください。

ExpressとFastifyの比較

NestJSは「プラットフォーム非依存」のフレームワークであり、HTTPプロバイダーとしてExpressまたはFastifyを選択できます。

パフォーマンス比較

項目 Express Fastify
リクエスト/秒 約15,000 約30,000
レイテンシ 約6.5ms 約3.2ms
メモリ使用量 標準 若干少ない
エコシステム 非常に豊富 成長中

Fastifyは、ベンチマーク結果でExpressの約2倍のパフォーマンスを発揮します。これはFastifyが、JSONスキーマによるシリアライゼーション最適化、効率的なルーティングアルゴリズム、そしてプラグインベースのアーキテクチャを採用しているためです。

移行時の考慮事項

Fastifyへ移行する際には以下の点に注意してください。

  • ミドルウェアの互換性: Express用のミドルウェアはそのままでは動作しません
  • リクエスト・レスポンスオブジェクト: Fastify固有のAPIを使用する必要があります
  • ファイルアップロード: @fastify/multipartなど専用パッケージが必要です

FastifyAdapterによる高性能化

パッケージのインストール

まず、Fastifyアダプターをインストールします。

1
npm install @nestjs/platform-fastify

main.tsの設定

ExpressからFastifyに切り替えるには、main.tsを以下のように変更します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

async function bootstrap() {
  // FastifyAdapterを使用してアプリケーションを作成
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );

  // Fastifyはデフォルトでlocalhost(127.0.0.1)のみをリッスン
  // 外部からのアクセスを許可する場合は'0.0.0.0'を指定
  await app.listen(process.env.PORT ?? 3000, '0.0.0.0');

  console.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

FastifyAdapterのオプション設定

FastifyAdapterのコンストラクタにオプションを渡すことで、Fastifyの詳細設定を行えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter({
      // ロギングを有効化
      logger: true,
      // リクエストボディの最大サイズ(デフォルト: 1MB)
      bodyLimit: 10485760, // 10MB
      // 信頼するプロキシの設定
      trustProxy: true,
    }),
  );

  await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
}
bootstrap();

Fastify用ミドルウェアの実装

Fastifyを使用する場合、ミドルウェアはFastifyRequestFastifyReplyの生オブジェクトを受け取ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// src/common/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(
    req: FastifyRequest['raw'],
    res: FastifyReply['raw'],
    next: () => void,
  ): void {
    const start = Date.now();

    res.on('finish', () => {
      const duration = Date.now() - start;
      console.log(`${req.method} ${req.url} - ${duration}ms`);
    });

    next();
  }
}

ルート設定とカスタムデコレータ

Fastifyでは@RouteConfig()@RouteConstraints()デコレータを使用して、ルートごとの設定を行えます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/users/users.controller.ts
import { Controller, Get, Req } from '@nestjs/common';
import { RouteConfig, RouteConstraints } from '@nestjs/platform-fastify';
import { FastifyRequest } from 'fastify';

@Controller('users')
export class UsersController {
  @Get()
  @RouteConfig({ output: 'users list' })
  findAll(@Req() req: FastifyRequest) {
    // routeConfigにアクセス可能
    console.log(req.routeOptions.config);
    return [];
  }

  @Get('v2')
  @RouteConstraints({ version: '2.x' })
  findAllV2() {
    // バージョン2.x向けのエンドポイント
    return { version: 2, users: [] };
  }
}

CacheModuleによるキャッシュ実装

キャッシュは頻繁にアクセスされるデータを一時的に保存し、データベースクエリや外部APIへのリクエストを削減することでパフォーマンスを向上させます。

パッケージのインストール

1
npm install @nestjs/cache-manager cache-manager

インメモリキャッシュの設定

最もシンプルなインメモリキャッシュの設定を行います。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// src/app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    CacheModule.register({
      // キャッシュの有効期限(ミリ秒)
      ttl: 5000,
      // 最大エントリ数
      max: 100,
      // グローバルモジュールとして登録
      isGlobal: true,
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Serviceでのキャッシュ利用

CACHE_MANAGERトークンを使用してキャッシュマネージャーをインジェクトします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// src/products/products.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';

interface Product {
  id: number;
  name: string;
  price: number;
}

@Injectable()
export class ProductsService {
  constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

  async findAll(): Promise<Product[]> {
    const cacheKey = 'products:all';

    // キャッシュから取得を試みる
    const cached = await this.cacheManager.get<Product[]>(cacheKey);
    if (cached) {
      console.log('Cache hit: products:all');
      return cached;
    }

    console.log('Cache miss: products:all');

    // データベースからデータを取得(実際の実装では Repository を使用)
    const products: Product[] = await this.fetchProductsFromDatabase();

    // キャッシュに保存(TTL: 60秒)
    await this.cacheManager.set(cacheKey, products, 60000);

    return products;
  }

  async findOne(id: number): Promise<Product | null> {
    const cacheKey = `products:${id}`;

    const cached = await this.cacheManager.get<Product>(cacheKey);
    if (cached) {
      return cached;
    }

    const product = await this.fetchProductFromDatabase(id);
    if (product) {
      await this.cacheManager.set(cacheKey, product, 60000);
    }

    return product;
  }

  async update(id: number, data: Partial<Product>): Promise<Product> {
    const product = await this.updateProductInDatabase(id, data);

    // 更新時はキャッシュを削除
    await this.cacheManager.del(`products:${id}`);
    await this.cacheManager.del('products:all');

    return product;
  }

  async clearCache(): Promise<void> {
    // キャッシュ全体をクリア
    await this.cacheManager.clear();
  }

  private async fetchProductsFromDatabase(): Promise<Product[]> {
    // 実際のデータベースクエリをシミュレート
    return [
      { id: 1, name: 'Product A', price: 1000 },
      { id: 2, name: 'Product B', price: 2000 },
    ];
  }

  private async fetchProductFromDatabase(id: number): Promise<Product | null> {
    const products = await this.fetchProductsFromDatabase();
    return products.find((p) => p.id === id) ?? null;
  }

  private async updateProductInDatabase(
    id: number,
    data: Partial<Product>,
  ): Promise<Product> {
    return { id, name: data.name ?? 'Updated', price: data.price ?? 0 };
  }
}

CacheInterceptorによる自動キャッシュ

CacheInterceptorを使用すると、GETエンドポイントのレスポンスを自動的にキャッシュできます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/products/products.controller.ts
import {
  Controller,
  Get,
  Param,
  ParseIntPipe,
  UseInterceptors,
} 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 readonly productsService: ProductsService) {}

  @Get()
  @CacheTTL(30000) // 30秒
  @CacheKey('all-products')
  findAll() {
    return this.productsService.findAll();
  }

  @Get(':id')
  @CacheTTL(60000) // 60秒
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.productsService.findOne(id);
  }
}

グローバルCacheInterceptorの設定

アプリケーション全体でキャッシュを有効にするには、APP_INTERCEPTORを使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_INTERCEPTOR } from '@nestjs/core';
import { CacheModule, CacheInterceptor } from '@nestjs/cache-manager';

@Module({
  imports: [
    CacheModule.register({
      ttl: 5000,
      isGlobal: true,
    }),
  ],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {}

Redisキャッシュの設定

本番環境では、スケーラブルなRedisキャッシュを使用することを推奨します。

1
npm install @keyv/redis keyv cacheable
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// src/app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule, ConfigService } from '@nestjs/config';
import KeyvRedis from '@keyv/redis';
import { Keyv } from 'keyv';
import { CacheableMemory } from 'cacheable';

@Module({
  imports: [
    ConfigModule.forRoot(),
    CacheModule.registerAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => {
        const redisUrl = configService.get<string>('REDIS_URL');

        return {
          stores: [
            // L1キャッシュ: インメモリ(高速)
            new Keyv({
              store: new CacheableMemory({
                ttl: 60000,
                lruSize: 5000,
              }),
            }),
            // L2キャッシュ: Redis(永続化・共有)
            new KeyvRedis(redisUrl ?? 'redis://localhost:6379'),
          ],
        };
      },
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

カスタムCacheInterceptorの実装

認証情報に基づいてキャッシュキーを生成するカスタムInterceptorを実装します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/common/interceptors/http-cache.interceptor.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { CacheInterceptor } from '@nestjs/cache-manager';
import { FastifyRequest } from 'fastify';

@Injectable()
export class HttpCacheInterceptor extends CacheInterceptor {
  trackBy(context: ExecutionContext): string | undefined {
    const request = context.switchToHttp().getRequest<FastifyRequest>();

    // POSTやPUT等はキャッシュしない
    if (request.method !== 'GET') {
      return undefined;
    }

    // ユーザーごとにキャッシュを分離
    const userId = (request as any).user?.id ?? 'anonymous';
    const url = request.url;

    return `${userId}:${url}`;
  }
}

レスポンス圧縮の設定

レスポンス圧縮は、転送データ量を削減してパフォーマンスを向上させます。

Fastify用圧縮パッケージのインストール

1
npm install @fastify/compress

圧縮の設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import compression from '@fastify/compress';
import { constants } from 'node:zlib';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );

  // 圧縮ミドルウェアを登録
  await app.register(compression, {
    // Brotli圧縮の品質設定(0-11、デフォルト: 11)
    // 値が低いほど高速だが圧縮率は低下
    brotliOptions: {
      params: {
        [constants.BROTLI_PARAM_QUALITY]: 4,
      },
    },
    // 圧縮の優先順位(Brotli > gzip > deflate)
    encodings: ['br', 'gzip', 'deflate'],
    // 閾値(このサイズ以下のレスポンスは圧縮しない)
    threshold: 1024, // 1KB
  });

  await app.listen(process.env.PORT ?? 3000, '0.0.0.0');
}
bootstrap();

圧縮のエンコーディング設定

高速レスポンスを優先する場合は、gzipとdeflateのみを使用します。

1
2
3
4
5
6
// src/main.ts
await app.register(compression, {
  // Brotliを使用せずgzipとdeflateのみを使用
  // レスポンスサイズは増加するが配信速度は向上
  encodings: ['gzip', 'deflate'],
});

本番環境での推奨設定

高トラフィックの本番環境では、アプリケーションサーバーではなくリバースプロキシ(Nginx等)で圧縮を行うことを推奨します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# nginx.conf
http {
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
    gzip_min_length 1024;

    # Brotli(ngx_brotli モジュールが必要)
    brotli on;
    brotli_comp_level 4;
    brotli_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml;
}

パフォーマンス計測と監視

パフォーマンスチューニングには、計測と監視が不可欠です。

レスポンスタイム計測Interceptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// src/common/interceptors/performance.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
  Logger,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { FastifyRequest } from 'fastify';

interface PerformanceMetrics {
  method: string;
  url: string;
  statusCode: number;
  duration: number;
  timestamp: Date;
}

@Injectable()
export class PerformanceInterceptor implements NestInterceptor {
  private readonly logger = new Logger(PerformanceInterceptor.name);
  private readonly metrics: PerformanceMetrics[] = [];

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest<FastifyRequest>();
    const response = context.switchToHttp().getResponse();
    const { method, url } = request;
    const startTime = Date.now();

    return next.handle().pipe(
      tap({
        next: () => {
          const duration = Date.now() - startTime;
          const statusCode = response.statusCode;

          // 警告閾値(500ms以上)
          if (duration > 500) {
            this.logger.warn(
              `Slow request: ${method} ${url} - ${duration}ms`,
            );
          }

          // メトリクスを記録
          this.metrics.push({
            method,
            url,
            statusCode,
            duration,
            timestamp: new Date(),
          });

          // メトリクス数を制限(直近1000件)
          if (this.metrics.length > 1000) {
            this.metrics.shift();
          }
        },
        error: (error) => {
          const duration = Date.now() - startTime;
          this.logger.error(
            `Error request: ${method} ${url} - ${duration}ms - ${error.message}`,
          );
        },
      }),
    );
  }

  getMetrics(): PerformanceMetrics[] {
    return [...this.metrics];
  }

  getAverageResponseTime(): number {
    if (this.metrics.length === 0) return 0;
    const total = this.metrics.reduce((sum, m) => sum + m.duration, 0);
    return total / this.metrics.length;
  }
}

メトリクスエンドポイントの実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/metrics/metrics.controller.ts
import { Controller, Get } from '@nestjs/common';
import { PerformanceInterceptor } from '../common/interceptors/performance.interceptor';

interface MetricsSummary {
  totalRequests: number;
  averageResponseTime: number;
  slowRequests: number;
  errorRate: number;
  uptime: number;
  memoryUsage: NodeJS.MemoryUsage;
}

@Controller('metrics')
export class MetricsController {
  private readonly startTime = Date.now();

  constructor(
    private readonly performanceInterceptor: PerformanceInterceptor,
  ) {}

  @Get()
  getMetrics(): MetricsSummary {
    const metrics = this.performanceInterceptor.getMetrics();
    const slowRequests = metrics.filter((m) => m.duration > 500).length;
    const errorRequests = metrics.filter((m) => m.statusCode >= 400).length;

    return {
      totalRequests: metrics.length,
      averageResponseTime: this.performanceInterceptor.getAverageResponseTime(),
      slowRequests,
      errorRate: metrics.length > 0 ? errorRequests / metrics.length : 0,
      uptime: Date.now() - this.startTime,
      memoryUsage: process.memoryUsage(),
    };
  }

  @Get('health')
  healthCheck() {
    return {
      status: 'ok',
      timestamp: new Date().toISOString(),
      uptime: process.uptime(),
    };
  }
}

メモリ使用量の監視

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// src/common/services/memory-monitor.service.ts
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';

interface MemorySnapshot {
  timestamp: Date;
  heapUsed: number;
  heapTotal: number;
  external: number;
  rss: number;
}

@Injectable()
export class MemoryMonitorService implements OnModuleInit {
  private readonly logger = new Logger(MemoryMonitorService.name);
  private readonly snapshots: MemorySnapshot[] = [];
  private intervalId: NodeJS.Timeout;

  onModuleInit() {
    // 30秒ごとにメモリ使用量を記録
    this.intervalId = setInterval(() => {
      this.recordSnapshot();
    }, 30000);
  }

  onModuleDestroy() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }

  private recordSnapshot(): void {
    const memoryUsage = process.memoryUsage();
    const snapshot: MemorySnapshot = {
      timestamp: new Date(),
      heapUsed: memoryUsage.heapUsed,
      heapTotal: memoryUsage.heapTotal,
      external: memoryUsage.external,
      rss: memoryUsage.rss,
    };

    this.snapshots.push(snapshot);

    // 直近100件のスナップショットを保持
    if (this.snapshots.length > 100) {
      this.snapshots.shift();
    }

    // ヒープ使用率が80%を超えた場合に警告
    const heapUsagePercent = (memoryUsage.heapUsed / memoryUsage.heapTotal) * 100;
    if (heapUsagePercent > 80) {
      this.logger.warn(
        `High memory usage: ${heapUsagePercent.toFixed(2)}% (${this.formatBytes(memoryUsage.heapUsed)} / ${this.formatBytes(memoryUsage.heapTotal)})`,
      );
    }
  }

  getSnapshots(): MemorySnapshot[] {
    return [...this.snapshots];
  }

  getCurrentUsage(): MemorySnapshot {
    const memoryUsage = process.memoryUsage();
    return {
      timestamp: new Date(),
      heapUsed: memoryUsage.heapUsed,
      heapTotal: memoryUsage.heapTotal,
      external: memoryUsage.external,
      rss: memoryUsage.rss,
    };
  }

  private formatBytes(bytes: number): string {
    const units = ['B', 'KB', 'MB', 'GB'];
    let unitIndex = 0;
    let value = bytes;

    while (value >= 1024 && unitIndex < units.length - 1) {
      value /= 1024;
      unitIndex++;
    }

    return `${value.toFixed(2)} ${units[unitIndex]}`;
  }
}

パフォーマンス最適化のベストプラクティス

データベースクエリの最適化

キャッシュと組み合わせて、N+1問題を回避します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// src/products/products.service.ts
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';

@Injectable()
export class ProductsService {
  constructor(
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
    @InjectRepository(Product)
    private productRepository: Repository<Product>,
  ) {}

  async findAllWithCategories(): Promise<Product[]> {
    const cacheKey = 'products:with-categories';
    const cached = await this.cacheManager.get<Product[]>(cacheKey);

    if (cached) {
      return cached;
    }

    // N+1問題を回避するJOINクエリ
    const products = await this.productRepository
      .createQueryBuilder('product')
      .leftJoinAndSelect('product.category', 'category')
      .leftJoinAndSelect('product.reviews', 'reviews')
      .orderBy('product.createdAt', 'DESC')
      .getMany();

    await this.cacheManager.set(cacheKey, products, 60000);

    return products;
  }
}

接続プールの設定

データベース接続プールを適切に設定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        type: 'postgres',
        host: configService.get('DB_HOST'),
        port: configService.get('DB_PORT'),
        username: configService.get('DB_USERNAME'),
        password: configService.get('DB_PASSWORD'),
        database: configService.get('DB_DATABASE'),
        autoLoadEntities: true,
        // 接続プール設定
        extra: {
          // 最大接続数
          max: 20,
          // 接続タイムアウト(ミリ秒)
          connectionTimeoutMillis: 5000,
          // アイドル接続のタイムアウト(ミリ秒)
          idleTimeoutMillis: 30000,
        },
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

完全な構成例

最終的なmain.tsの完全な構成例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// src/main.ts
import { NestFactory } from '@nestjs/core';
import {
  FastifyAdapter,
  NestFastifyApplication,
} from '@nestjs/platform-fastify';
import compression from '@fastify/compress';
import { constants } from 'node:zlib';
import { ValidationPipe, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AppModule } from './app.module';
import { PerformanceInterceptor } from './common/interceptors/performance.interceptor';

async function bootstrap() {
  const logger = new Logger('Bootstrap');

  // FastifyAdapterで高速なHTTPサーバーを構築
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter({
      logger: true,
      bodyLimit: 10485760, // 10MB
      trustProxy: true,
    }),
  );

  const configService = app.get(ConfigService);

  // レスポンス圧縮
  await app.register(compression, {
    brotliOptions: {
      params: {
        [constants.BROTLI_PARAM_QUALITY]: 4,
      },
    },
    encodings: ['br', 'gzip', 'deflate'],
    threshold: 1024,
  });

  // グローバルバリデーション
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  // パフォーマンス計測
  const performanceInterceptor = app.get(PerformanceInterceptor);
  app.useGlobalInterceptors(performanceInterceptor);

  // CORS設定
  app.enableCors({
    origin: configService.get<string>('CORS_ORIGIN', '*'),
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    credentials: true,
  });

  // グレースフルシャットダウン
  app.enableShutdownHooks();

  const port = configService.get<number>('PORT', 3000);
  await app.listen(port, '0.0.0.0');

  logger.log(`Application is running on: ${await app.getUrl()}`);
}
bootstrap();

パフォーマンス改善の効果測定

以下のような改善効果が期待できます。

graph LR
    A[Express] -->|ベースライン| B[15,000 req/s]
    C[Fastify] -->|+100%| D[30,000 req/s]
    E[+キャッシュ] -->|+200%| F[45,000 req/s]
    G[+圧縮] -->|転送量-70%| H[帯域節約]
最適化項目 改善効果
FastifyAdapter スループット約2倍
インメモリキャッシュ DB負荷50-90%削減
Redisキャッシュ スケールアウト対応
レスポンス圧縮 転送量60-80%削減
接続プール最適化 接続オーバーヘッド削減

まとめ

本記事では、NestJSアプリケーションのパフォーマンスチューニングについて解説しました。

  • FastifyAdapter: Expressから切り替えることで約2倍のスループット向上
  • CacheModule: インメモリキャッシュとRedisキャッシュによるDB負荷削減
  • レスポンス圧縮: Brotli/gzipによる転送データ量の大幅削減
  • パフォーマンス監視: 計測とモニタリングによる継続的な改善

これらのテクニックを適切に組み合わせることで、高負荷環境でも安定して動作するNestJSアプリケーションを構築できます。本番環境への適用前には、必ず負荷テストを実施してボトルネックを特定し、段階的に最適化を進めてください。

参考リンク