NestJS 11 は、Angular にインスピレーションを得たアーキテクチャと Node.js のパワーを組み合わせた、TypeScript で本番グレードの API を構築するための頼りになるフレームワークとして登場しました。エンタープライズ規模で運用している場合、数百万のリクエストを処理し、マルチテナント データを管理し、数十のモジュールを調整している場合、初日に選択したパターンによって、コードベースが適切に拡張されるか、それともコードベース自体の重みで崩壊するかが決まります。
このガイドでは、310 以上の TypeScript ファイルを含む 56 モジュールの NestJS 11 バックエンドの構築から苦労して得た教訓を抽出し、モジュールの構成やガード構成から、実際に負荷に耐えられるマルチテナント パターンまですべてをカバーしています。
重要なポイント
forRoutes('*')ではなくforRoutes('*path')を使用する — NestJS 11 ではワイルドカード ルート マッチングが変更されました- グローバル例外フィルターは
APP_FILTERではなくmain.tsに登録する必要があります- マルチテナントでは、ミドルウェアだけでなく、すべてのクエリ層で
organizationIdフィルタリングが必要です@Public()デコレータ パターンは、オープン ルートのガードを完全に無効にするより安全です- Drizzle クエリでは
sql.raw()を決して使用しないでください。常にパラメーター化されたsqlテンプレート リテラルを使用します。- すべての機能モジュールでの再インポートを避けるために、EmailModule は
@Global()である必要があります- 環境解決の問題を避けるために、NestJS ブートストラップの前に Dotenv を
main.tsにプリロードする必要があります- レート制限はすべてのパブリック エンドポイントで必須です —
@nestjs/throttlerを使用してください
大規模なプロジェクト構造
NestJS で最も重要なアーキテクチャ上の決定は、モジュールをどのように編成するかです。エンタープライズ規模では、app.module.ts のフラットなモジュール リストは管理できなくなります。機能するパターンは、明示的な依存関係宣言を使用したドメイン駆動型のモジュール グループ化です。
apps/api/src/
modules/
auth/
auth.module.ts
auth.controller.ts
auth.service.ts
guards/
jwt.guard.ts
roles.guard.ts
decorators/
public.decorator.ts
roles.decorator.ts
contacts/
contacts.module.ts
contacts.controller.ts
contacts.service.ts
contacts.spec.ts
dto/
create-contact.dto.ts
update-contact.dto.ts
billing/
billing.module.ts
billing.service.ts
webhook.controller.ts
shared/
filters/
global-exception.filter.ts
interceptors/
transform.interceptor.ts
pipes/
validation.pipe.ts
health/
health.controller.ts
indicators/
main.ts
app.module.ts
各ドメイン モジュールは自己完結型です。 shared/ ディレクトリには、横断的な懸念事項が含まれています。この分離により、既存のコードに触れることなく新しいドメインを追加できます。
@Public() デコレータ パターン
NestJS 11 は JWT ガードをグローバルに適用しますが、ヘルスチェック、認証コールバック、Webhook レシーバーなど、選択したエンドポイントを開く必要があります。 @Public() デコレータ パターンは、ルートごとにガードを無効にするよりもはるかに優れています。
// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
// guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
return super.canActivate(context);
}
}
// app.module.ts
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{ provide: APP_GUARD, useClass: JwtAuthGuard },
{ provide: APP_GUARD, useClass: RolesGuard },
],
})
export class AppModule {}
@Public() で装飾されたコントローラー メソッドは、JWT 検証を完全にバイパスするようになりました。 Webhook コントローラー、ヘルス エンドポイント、および認証ルートはすべてこのパターンを使用します。
OrganizationId を使用したマルチテナント
マルチテナント API の基本ルール: すべてのデータベース クエリは organizationId でフィルターする必要があります。 req.organizationId を設定するミドルウェアだけでは十分ではありません。開発者がフィルターの適用を忘れると、クロステナント データが公開されてしまいます。
パターンは、JWT ペイロードから organizationId を抽出し、それを型指定されたリクエスト インターフェイスに添付することです。
// types/authenticated-request.interface.ts
import { Request } from 'express';
export interface AuthenticatedRequest extends Request {
user: {
sub: string;
email: string;
name: string;
role: 'admin' | 'support' | 'user';
organizationId: string;
};
}
// contacts/contacts.controller.ts
import { Controller, Get, Post, Body, Req } from '@nestjs/common';
import { AuthenticatedRequest } from '../../types/authenticated-request.interface';
@Controller('contacts')
export class ContactsController {
constructor(private contactsService: ContactsService) {}
@Get()
findAll(@Req() req: AuthenticatedRequest) {
return this.contactsService.findAll(req.user.organizationId);
}
@Post()
create(@Body() dto: CreateContactDto, @Req() req: AuthenticatedRequest) {
return this.contactsService.create(dto, req.user.organizationId);
}
}
// contacts/contacts.service.ts
import { db } from '@ecosire/db';
import { contacts } from '@ecosire/db/schema';
import { eq, and } from 'drizzle-orm';
@Injectable()
export class ContactsService {
async findAll(organizationId: string) {
return db
.select()
.from(contacts)
.where(eq(contacts.organizationId, organizationId))
.limit(100);
}
}
サービス層はクエリ レベルで organizationId を強制します。 SQL 自体が分離を強制するため、ミドルウェアの障害やデコレーターの忘れによっては、テナント間データが漏洩する可能性はありません。
グローバル例外フィルター
すべてのエンドポイントにわたって一貫したエラー応答を実現するには、グローバル例外フィルターが必要です。 NestJS 11 の HttpException 階層はほとんどのケースに対応しますが、予期しないエラーをキャッチし、本番環境でスタック トレースを決して公開しないようにする必要もあります。
// filters/global-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
Logger,
} from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let errors: Record<string, string[]> | undefined;
if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
message = exceptionResponse;
} else if (typeof exceptionResponse === 'object') {
const resp = exceptionResponse as Record<string, unknown>;
message = (resp.message as string) || message;
if (Array.isArray(resp.message)) {
// Validation errors from class-validator
errors = this.formatValidationErrors(resp.message as string[]);
message = 'Validation failed';
}
}
} else if (exception instanceof Error) {
this.logger.error(exception.message, exception.stack);
// Never expose stack traces in production
if (process.env.NODE_ENV !== 'production') {
message = exception.message;
}
}
response.status(status).json({
statusCode: status,
message,
errors,
path: request.url,
timestamp: new Date().toISOString(),
});
}
private formatValidationErrors(messages: string[]): Record<string, string[]> {
const errors: Record<string, string[]> = {};
for (const msg of messages) {
const [field, ...rest] = msg.split(' ');
if (!errors[field]) errors[field] = [];
errors[field].push(rest.join(' '));
}
return errors;
}
}
main.ts に登録します。
// main.ts
import { NestFactory } from '@nestjs/core';
import { GlobalExceptionFilter } from './shared/filters/global-exception.filter';
async function bootstrap() {
// CRITICAL: Load env vars before NestJS bootstraps
require('dotenv').config({ path: join(__dirname, '..', '..', '..', '.env.local') });
const app = await NestFactory.create(AppModule);
app.useGlobalFilters(new GlobalExceptionFilter());
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
await app.listen(3001);
}
NestJS 11 ルートのワイルドカードの変更
NestJS 11 の最も重大な変更の 1 つは、ワイルドカード ルート マッチングです。 NestJS 10 から移行している場合、これによりミドルウェアが静かに破壊されます。
// NestJS 10 — works
consumer.apply(LoggerMiddleware).forRoutes('*');
// NestJS 11 — use '*path' instead
consumer.apply(LoggerMiddleware).forRoutes('*path');
同じことが、Swagger セットアップと文字列ベースのルート パターンにも当てはまります。この変更は、ミドルウェアの登録、ルートの除外、およびグロブ スタイルのルート マッチングを使用する場所に影響します。
ロールベースのアクセス制御
JWT 認証以外にも、エンタープライズ API にはロールベースの承認が必要です。デコレーターとガードのパターンにより、コントローラーがクリーンに保たれます。
// decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export type Role = 'admin' | 'support' | 'user';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
// guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY, Role } from '../decorators/roles.decorator';
import { AuthenticatedRequest } from '../../types/authenticated-request.interface';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) return true;
const { user } = context.switchToHttp().getRequest<AuthenticatedRequest>();
const hasRole = requiredRoles.some((role) => user.role === role);
if (!hasRole) {
throw new ForbiddenException('Insufficient permissions');
}
return true;
}
}
コントローラーでの使用法:
// For top-level module controllers — one `..`
import { Roles } from '../auth/guards/roles.guard';
// For nested sub-module controllers — two `../..`
import { Roles } from '../../auth/guards/roles.guard';
@Controller('admin/contacts')
@Roles('admin', 'support')
export class AdminContactsController {}
インポート パスの深さはバグの一般的な原因です。トップレベルのモジュール コントローラーは 1 つの .. を使用します。ネストされたサブモジュール コントローラーは 2 つの ../.. を使用します。
グローバル EmailModule パターン
@Global() デコレータは、あらゆる場所で使用されるサービスの繰り返しインポートの問題を解決します。電子メールは標準的な使用例です。通知を送信するすべての機能モジュールに EmailModule をインポートする必要はありません。
// email/email.module.ts
import { Global, Module } from '@nestjs/common';
import { EmailService } from './email.service';
@Global()
@Module({
providers: [EmailService],
exports: [EmailService],
})
export class EmailModule {}
AppModule に一度登録します。
@Module({
imports: [
EmailModule, // Global — available everywhere
ContactsModule,
BillingModule,
// ...
],
})
export class AppModule {}
これで、どのサービスもモジュールのインポートに触れることなく EmailService を挿入できるようになりました。同じパターンは、Redis、EventBus、およびあらゆる横断的なインフラストラクチャ サービスで機能します。
レート制限パブリック エンドポイント
エンタープライズ API は毎日悪用の試みに直面しています。 NestJS の @nestjs/throttler はガード システムときれいに統合されています。
// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
@Module({
imports: [
ThrottlerModule.forRoot([
{ name: 'short', ttl: 1000, limit: 10 },
{ name: 'medium', ttl: 60000, limit: 100 },
]),
],
providers: [
{ provide: APP_GUARD, useClass: ThrottlerGuard },
],
})
エンドポイントごとに @Throttle() をオーバーライドして、機密ルートの制限を厳しくします。
import { Throttle } from '@nestjs/throttler';
@Controller('auth')
export class AuthController {
@Post('exchange')
@Public()
@Throttle({ short: { ttl: 60000, limit: 5 } }) // 5 per minute
async exchangeCode(@Body() dto: ExchangeCodeDto) {
return this.authService.exchangeCode(dto.code);
}
}
@nestjs/terminus によるヘルスチェック
実稼働 API には、ロード バランサーのチェックと監視のためのヘルス エンドポイントが必要です。 @nestjs/terminus は、組み込みインジケーターを備えた宣言型システムを提供します。
// health/health.controller.ts
import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, TypeOrmHealthIndicator } from '@nestjs/terminus';
import { Public } from '../auth/decorators/public.decorator';
import { DatabaseHealthIndicator } from './indicators/database.indicator';
import { RedisHealthIndicator } from './indicators/redis.indicator';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: DatabaseHealthIndicator,
private redis: RedisHealthIndicator,
) {}
@Get()
@Public()
@HealthCheck()
check() {
return this.health.check([
() => this.db.isHealthy('database'),
() => this.redis.isHealthy('redis'),
]);
}
}
Drizzle ORM のカスタム インジケーター (ターミナルには組み込みがないため):
// health/indicators/database.indicator.ts
import { Injectable } from '@nestjs/common';
import { HealthIndicator, HealthIndicatorResult, HealthCheckError } from '@nestjs/terminus';
import { db } from '@ecosire/db';
import { sql } from 'drizzle-orm';
@Injectable()
export class DatabaseHealthIndicator extends HealthIndicator {
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
await db.execute(sql`SELECT 1`);
return this.getStatus(key, true);
} catch (error) {
throw new HealthCheckError('Database check failed', this.getStatus(key, false));
}
}
}
DTO 検証パターン
ValidationPipe と whitelist: true は、不明なプロパティがサービス層に到達する前に削除します。 class-transformer と組み合わせると、DTO が防御の第一線となります。
// contacts/dto/create-contact.dto.ts
import { IsString, IsEmail, IsOptional, IsEnum, MinLength, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
export enum ContactType {
INDIVIDUAL = 'individual',
COMPANY = 'company',
}
export class CreateContactDto {
@ApiProperty({ example: 'Acme Corp' })
@IsString()
@MinLength(2)
@MaxLength(255)
@Transform(({ value }) => value?.trim())
name: string;
@ApiProperty({ example: '[email protected]' })
@IsEmail()
@Transform(({ value }) => value?.toLowerCase().trim())
email: string;
@ApiPropertyOptional({ enum: ContactType })
@IsOptional()
@IsEnum(ContactType)
type?: ContactType;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(500)
notes?: string;
}
@Transform デコレータは検証層でデータを正規化します。空白と小文字の電子メールをトリミングすると、大文字と小文字の違いによる重複レコードが防止されます。
Swagger の統合
エンタープライズ API にはドキュメントが必要です。 NestJS の Swagger モジュールはデコレータから OpenAPI 仕様を生成しますが、セットアップは意図的に行う必要があります。
// main.ts
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('ECOSIRE API')
.setDescription('Enterprise API for ECOSIRE platform')
.setVersion('1.0')
.addBearerAuth()
.addTag('auth', 'Authentication endpoints')
.addTag('contacts', 'Contact management')
.build();
const document = SwaggerModule.createDocument(app, config);
// Only expose in non-production environments
if (process.env.NODE_ENV !== 'production') {
SwaggerModule.setup('api/docs', app, document);
}
await app.listen(3001);
}
すべてのコントローラー メソッドには @ApiOperation と @ApiResponse が必要です。
@Get(':id')
@ApiOperation({ summary: 'Get contact by ID' })
@ApiResponse({ status: 200, description: 'Contact found', type: ContactResponseDto })
@ApiResponse({ status: 404, description: 'Contact not found' })
async findOne(@Param('id') id: string, @Req() req: AuthenticatedRequest) {
return this.contactsService.findOne(id, req.user.organizationId);
}
よくある落とし穴と解決策
落とし穴 1: モジュール間の循環依存関係
循環インポートでは、起動時に不可解なエラーが発生して NestJS がクラッシュします。修正は forwardRef() です。
@Module({
imports: [forwardRef(() => BillingModule)],
})
export class LicenseModule {}
より良い解決策: 共有ロジックを 3 番目のモジュールに抽出し、両方とも循環依存関係を持たずにインポートします。
落とし穴 2: モジュールの初期化中に環境変数を使用できない
dotenv がロードされる前にクラス コンストラクターまたはプロバイダー ファクトリで process.env.DATABASE_URL にアクセスすると、undefined が取得されます。解決策: NestJS インポートの前に、main.ts の一番上で dotenv をロードします。
// main.ts — must be FIRST
import * as path from 'path';
require('dotenv').config({ path: path.join(__dirname, '..', '..', '..', '.env.local') });
// Then NestJS imports
import { NestFactory } from '@nestjs/core';
落とし穴 3: ライフサイクル フックの非同期操作で await が欠落している
// Wrong — database connection might not be ready
@Injectable()
export class AppService implements OnModuleInit {
onModuleInit() {
this.seedDatabase(); // Not awaited!
}
}
// Correct
async onModuleInit() {
await this.seedDatabase();
}
落とし穴 4: Drizzle クエリでの sql.raw() の使用
sql.raw() はパラメータ化をバイパスし、SQL インジェクション ベクトルを開きます。常に sql テンプレート リテラルを使用してください。
// Dangerous — never do this
const result = await db.execute(sql.raw(`SELECT * FROM contacts WHERE id = '${id}'`));
// Safe — parameterized
const result = await db.execute(sql`SELECT * FROM contacts WHERE id = ${id}`);
よくある質問
NestJS 11 API でバージョン管理を処理するにはどうすればよいですか?
NestJS 11 は、すぐに使える URI バージョン管理、ヘッダー バージョン管理、メディア タイプ バージョン管理をサポートしています。 main.ts で app.enableVersioning({ type: VersioningType.URI }) を使用してこれを有効にし、@Version('1') でコントローラーを装飾します。エンタープライズ API の場合、URI バージョン管理 (/v1/contacts) が最も明示的でキャッシュに優しいアプローチです。
NestJS でファイルのアップロードを処理する最適な方法は何ですか?
ファイルのアップロードには @nestjs/platform-express を multer とともに使用します。 S3 アップロードの場合は、multer-s3 を介してカスタム ストレージ エンジンを構成します。ハンドラーを実行する前に常にファイルのタイプとサイズをパイプ レベルで検証し、クライアントが提供する MIME タイプを決して信頼せず、代わりにマジック バイトを検証します。
NestJS でデータベース トランザクションをどのように構成すればよいですか?
Drizzle ORM は db.transaction(async (tx) => { ... }) によるトランザクションをサポートします。グローバル db インスタンスではなく、トランザクション オブジェクトをサービス メソッドに渡します。マルチオペレーションのビジネス ロジック (注文の作成 + 在庫の差し引き + 電子メールの送信) の場合は、すべてをトランザクションにラップし、コミット後に .catch() を使用して電子メール送信を非ブロックにします。
ガード、ミドルウェア、インターセプターのどのような場合に使用すべきですか?
ガードは承認を処理します (このユーザーはこのリソースにアクセスできますか?)。ミドルウェアは、横断的なリクエスト変換 (ロギング、相関 ID、解析) を処理します。インターセプターは、変換、キャッシュ、メトリクスの要求と応答のサイクルをラップします。実行順序は、ミドルウェア → ガード → インターセプター (前) → パイプ → ハンドラー → インターセプター (後) → 例外フィルターです。
NestJS モジュールを単独でテストするにはどうすればよいですか?
Test.createTestingModule() を使用して、モック化された依存関係を含むテスト サンドボックスを作成します。 jest.fn() または vi.fn() を使用してサービス メソッドをモックし、データベースとは独立してコントローラーの動作をテストします。統合テストの場合は、実際のデータベース接続 (別個のテスト データベース) で @nestjs/testing を使用し、各テスト後にトランザクション ロールバックを行います。
グローバル ガードのパフォーマンスへの影響は何ですか?
グローバル ガードはすべてのリクエストで実行されるため、高速に保ちます。 JWT 検証は通常 1 ~ 5 ミリ秒です。ガードでのデータベース検索を回避します。トークンの作成中に権限を読み込み、JWT ペイロードに含めます。すべてのリクエストに対して新しいアクセス許可が必要な場合は、データベースにアクセスするのではなく、短い TTL で Redis を使用してください。
次のステップ
エンタープライズ API を大規模に構築するには、初日から適切なアーキテクチャが必要です。 ECOSIRE のエンジニアリング チームは、複雑なマルチテナント ワークフロー、Stripe 請求、ライセンス管理、AI を活用した分析を処理する 56 モジュールの NestJS 11 バックエンドを構築、運用してきました。
カスタム NestJS API、Odoo ERP 統合、またはフルスタックのエンタープライズ プラットフォームが必要な場合でも、当社のチームは本番環境で実証済みのパターンをプロジェクトにもたらします。 開発サービスを探索 して、次のビルドを加速する方法を確認してください。
執筆者
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
関連記事
マルチテナント 2026 向けの Drizzle ORM + Postgres 行レベル セキュリティ
Drizzle ORM と Postgres 行レベル セキュリティを使用してマルチテナント SaaS を実装します: スキーマ、ポリシー、セッション変数、NestJS 統合、実際の運用パターン。
Odoo ORM API チートシート 2026: 検索、読み取り、書き込み、作成
例を含む実践的な Odoo ORM チートシート: 検索、参照、読み取り、書き込み、作成、リンク解除、レコードセット、ドメイン、計算フィールド、パフォーマンスのヒント。
Odoo Python デコレーター ガイド: 依存、制約、変更時
Odoo デコレータの完全ガイド: api.depends、api.constrains、api.onchange、api.ondelete、api.model、api.model_create_multi — それぞれが起動するとき。