バックエンド開発ガイド
このガイドでは、MBC CQRS Serverlessフレームワークを使用したバックエンドアプリケーション構築の包括的なパターンとベストプラクティスを提供します。例は本番プロジェクトから一般化されています。
モジュール構造
標準モジュールレイアウト
すべてのドメインモジュールは、この一貫した構造に従います:
src/[domain]/
├── dto/
│ ├── [domain]-command.dto.ts # Command input validation
│ ├── [domain]-attributes.dto.ts # Domain-specific attributes
│ └── [domain]-search.dto.ts # Search parameters
├── entity/
│ ├── [domain]-command.entity.ts # Command entity
│ ├── [domain]-data.entity.ts # Data entity
│ └── [domain]-data-list.entity.ts # List wrapper
├── handler/
│ └── [domain]-rds.handler.ts # RDS sync handler
├── [domain].service.ts # Business logic
├── [domain].controller.ts # HTTP handlers
└── [domain].module.ts # Module definition
モジュール登録
CommandModuleにモジュールを登録してCQRS機能を有効にします:
// product.module.ts
import { Module } from '@nestjs/common';
import { CommandModule } from '@mbc-cqrs-serverless/core';
import { ProductService } from './product.service';
import { ProductController } from './product.controller';
import { ProductDataSyncRdsHandler } from './handler/product-rds.handler';
@Module({
imports: [
CommandModule.register({
tableName: 'product',
dataSyncHandlers: [ProductDataSyncRdsHandler],
}),
],
controllers: [ProductController],
providers: [ProductService],
exports: [ProductService],
})
export class ProductModule {}
エンティティ設計
コマンドエンティティ
コマンドエンティティは書き込み操作を表し、バージョン追跡を含みます:
// entity/product-command.entity.ts
import { CommandEntity } from '@mbc-cqrs-serverless/core';
import { ProductAttributes } from '../dto/product-attributes.dto';
export class ProductCommandEntity extends CommandEntity {
attributes: ProductAttributes;
}
データエンティティ
データエンティティは処理後の読み取りモデルを表します:
// entity/product-data.entity.ts
import { DataEntity } from '@mbc-cqrs-serverless/core';
import { ProductAttributes } from '../dto/product-attributes.dto';
export class ProductDataEntity extends DataEntity {
attributes: ProductAttributes;
}
// entity/product-data-list.entity.ts
import { DataListEntity } from '@mbc-cqrs-serverless/core';
import { ProductDataEntity } from './product-data.entity';
export class ProductDataListEntity extends DataListEntity<ProductDataEntity> {
items: ProductDataEntity[];
}
属性DTO
バリデーション付きでドメイン固有の属性を定義します:
// dto/product-attributes.dto.ts
import { IsString, IsNumber, IsOptional, ValidateNested, Type } from 'class-validator';
export class ProductAttributes {
@IsString()
@IsOptional()
category?: string;
@IsNumber()
@IsOptional()
price?: number;
@IsString()
@IsOptional()
description?: string;
@Type(() => ProductSpecification)
@ValidateNested()
@IsOptional()
specification?: ProductSpecification;
}
export class ProductSpecification {
@IsString()
@IsOptional()
weight?: string;
@IsString()
@IsOptional()
dimensions?: string;
}
コントローラーパターン
標準コントローラー
コントローラーは薄く保ち、ビジネスロジックはサービスに委譲すべきです:
// product.controller.ts
import {
Controller,
Get,
Post,
Put,
Body,
Param,
Query,
} from '@nestjs/common';
import { INVOKE_CONTEXT, IInvoke, SearchDto } from '@mbc-cqrs-serverless/core';
import { ProductService } from './product.service';
import { ProductCommandDto } from './dto/product-command.dto';
import { ProductDataEntity, ProductDataListEntity } from './entity';
@Controller('api/product')
export class ProductController {
constructor(private readonly productService: ProductService) {}
/**
* Create or update a product
*/
@Post('/')
async publishCommand(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Body() cmdDto: ProductCommandDto,
): Promise<ProductDataEntity> {
return this.productService.publishCommand(cmdDto, invokeContext);
}
/**
* Bulk create/update products
*/
@Post('/bulk')
async publishBulkCommands(
@INVOKE_CONTEXT() invokeContext: IInvoke,
@Body() cmdDtos: ProductCommandDto[],
): Promise<ProductDataEntity[]> {
return this.productService.publishBulkCommands(cmdDtos, invokeContext);
}
/**
* Get product by PK and SK
*/
@Get('data/:pk/:sk')
async getData(
@Param('pk') pk: string,
@Param('sk') sk: string,
): Promise<ProductDataEntity> {
return this.productService.getData(pk, sk);
}
/**
* List products by PK
*/
@Get('data/:pk')
async listDataByPk(
@Param('pk') pk: string,
@Query() searchDto: SearchDto,
): Promise<ProductDataListEntity> {
return this.productService.listDataByPk(pk, searchDto);
}
/**
* Search products
*/
@Get('data')
async searchData(
@Query() searchDto: SearchDto,
): Promise<ProductDataListEntity> {
return this.productService.searchData(searchDto);
}
/**
* すべてのデータをRDSに再同期
*/
@Put('resync-data/:pk')
async resyncData(@Param('pk') pk: string): Promise<void> {
return this.productService.resyncData(pk);
}
}
サービス実装
基本サービスパターン
サービスにはビジネスロジックが含まれ、データ操作を調整します。最小限の例を示します:
// product.service.ts
import { Injectable, Logger } from '@nestjs/common';
import {
CommandService,
DataService,
IInvoke,
KEY_SEPARATOR,
generateId,
getUserContext,
VERSION_FIRST,
} from '@mbc-cqrs-serverless/core';
import { ulid } from 'ulid';
import { PrismaService } from '../prisma/prisma.service';
import { ProductCommandDto } from './dto/product-command.dto';
import { ProductDataEntity } from './entity';
const PRODUCT_PK_PREFIX = 'PRODUCT';
@Injectable()
export class ProductService {
private readonly logger = new Logger(ProductService.name);
constructor(
private readonly commandService: CommandService,
private readonly dataService: DataService,
private readonly prismaService: PrismaService,
) {}
/**
* Create a new product (新しい商品を作成)
*/
async create(
createDto: { name: string; description?: string },
opts: { invokeContext: IInvoke },
): Promise<ProductDataEntity | null> {
const { tenantCode } = getUserContext(opts.invokeContext);
const pk = `${PRODUCT_PK_PREFIX}${KEY_SEPARATOR}${tenantCode}`;
const sk = ulid();
const command = new ProductCommandDto({
pk,
sk,
id: generateId(pk, sk),
tenantCode,
code: sk,
type: 'PRODUCT',
name: createDto.name,
version: VERSION_FIRST,
attributes: { description: createDto.description },
});
const item = await this.commandService.publishAsync(command, {
invokeContext: opts.invokeContext,
});
// publishAsync は変更がない場合(no-op)null を返す
if (!item) return null;
return new ProductDataEntity(item);
}
/**
* Get product by key (キーで商品を取得)
*/
async findOne(pk: string, sk: string): Promise<ProductDataEntity | null> {
const item = await this.dataService.getItem({ pk, sk });
if (!item) return null;
return new ProductDataEntity(item);
}
}
完全なServiceパターンについて
包括的なCRUD操作、バッチ処理、楽観的ロック、およびその他の高度なパターンについては、Service実装パターンを参照してください。