はじめに
お久しぶりです。
アイスタイルT&C部に所属しているokashitayというものです。
2023年にNestJSの本番稼働してから1年以上が経過し、2024年11月現在、運用・保守・開発は特に問題なく安定して進められています。
今回は現在の状況と、上記の2022年当時の記事では触れきれなかったNestJSを利用したことについてを振り返るとともに少し掘り下げて記載したいと思います。
2024年 現在の構成
オンプレミスからクラウドへの移行が着実に進んでいます。ミドルウェア周りの移行はほぼ完了し、残るはSQL ServerからTiDBへの移行と、RabbitMQのクラウド化となります。
- Aurora MySQL
マイクロサービス化に伴い導入されたデータベース - SQLServer
20年以上前から@cosme
で使用され続けているデータベース - OpenSearch
要件に応じたインデックスを作成して、高速で柔軟な検索に活用しています。 - MSK(kafka)
MSKにmessageを送信、後続するアプリケーションがストリーミング処理をし利用 - ElastiCache
一部のAPIエンドポイントにてキャッシュ処理に使用 - RabbitMQ
messageを送信、後続するアプリケーションがストリーミング処理をし利用
上記ミドルウェアを利用しアプリケーション自体も大規模な構造となっております。
フレームワーク選定の葛藤
当時、Node.jsを用いたアプリケーションの事例は数多く存在しており、Node.js自体を採用することに対する不安は特にありませんでした。一方で、NestJSを採用することには多少の不安が伴いました。
言語やフレームワークの選定においては、チーム状況や時代のトレンドに影響される部分もあり、何が最適な選択なのかを判断するのは採用時点では試行錯誤の連続だったと感じています。
選定にあたっては、採用するフレームワークが要件を満たしているか、パフォーマンスに問題がないか、そして開発チームに適合し、運用・保守・開発をスムーズに進められるかといった観点で検討を重ねました。
最終的な決め手となったのは、2022年の記事に記載している内容に加え大規模なアプリケーションで依存性注入(DI)やモジュール設計の整合性が高く当時のチーム、組織構成に最も適していた点だったでしょうか。
といっても悩む点はいろいろあると思います。その一つORM...
TypeORM採用の背景と複雑なSQLへのアプローチ
一番悩んでいた点としてはORMの選別でしょうか、
当時、TypeORMを採用するかPrismaを採用するかで大いに悩みました。
(npm trendsによると2023年からPrismaが伸びてきていますね、気になります)
TypeORMを利用する方針で進める事にしたのですが、アプリケーションが発行するSQLクエリも非常に複雑で、ORMマッパー機能で担保できるか不明なためSQLクエリをシンプルにTypeORMを介してデータベースに流し込むという方法をとることにより、問題が発生した際に迅速に調査しやすいよう進め、いざとなれば別のORMライブラリに切り替えられる様に考えたりもしていました。
具体的に以下の様な実装を行い、TypeORMによりクエリーを流し込んでいます。
@Injectable()
export class ProductRepository implements IProductRepository {
/**
* データを取得する
* @param productId number
* @param queryRunner QueryRunner
* @returns Promise<ProductEntity[]>
*/
async findProduct(
productId: number,
queryRunner: QueryRunner,
): Promise<ProductEntity[]> {
// SQL文とバインド値を取得
const [query, bindings]: [string, number[]] =
this.prepareFindDetailQuery(productId);
const product: unknown[] = await queryRunner.query(query, bindings);
const productMapper: ProductMapper = new ProductMapper();
// データ取得時はunknow型を適切な型、エンティティに
const productEntity: ProductEntity[] = product.map((_) =>
productMapper.buildEntity(_),
);
// 取得したデータはよしなに
return productEntity;
}
/**
* 取得用SQL文とバインドする値の配列を生成する
*
* @param productId number 商品ID
* @returns [string, number[]] [SQL文, [バインド値の配列]]
*/
private prepareFindDetailQuery(productId: number): [string, number[]] {
const bindings: number[] = [productId];
// 実際には複雑なクエリになっています
const query = `
SELECT
...
FROM products p
INNER JOIN XXXX ON XXX
....
WHERE AND XXXXX
AND p.id = ?
`;
return [query, bindings];
}
}
export interface ProductEntity {
product_id: number;
product_name: string;
}
export class ProductMapper {
private static readonly ERROR_INVALID_ENTITY = {
code: 500,
error: 'type is invalid',
};
buildEntity(object: unknown): ProductEntity {
if (!this.isEntity(object)) {
throw new InternalServerErrorException(ProductMapper.ERROR_INVALID_ENTITY);
}
return {
product_id: object.product_id,
product_name: object.product_name,
};
}
isEntity(value: unknown): value is ProductEntity {
return (
value !== null &&
typeof value === 'object' &&
typeof (value as ProductData).product_id === 'number' &&
typeof (value as ProductData).product_name === 'string'
);
}
}
TypeORM自体が今後、バージョンアップによる破壊的な変更も行われていくことも懸念しORMの機能をフル活用する事によってバージョンアップに工数を割きたくないというのもあったと思います。
バランスよくリスクに備え構築していくのが良さそうでしょうか。
最後に
1年以上、大規模バックエンドAPIにNestJSを用いて実運用していますが、エラーで障害になる事もなく、社内基準SLO・SLAは現在策定中ですが稼働率やパフォーマンスも社内基準の値を十分担保できていると思われます。
今後も安定した運用を維持しユーザーに快適なサービス体験を提供するため、的確に情報をキャッチアップしつつ、安全にNode.jsやNestJSの利用をしていきたいと思います。
もちろん他の技術検証もしつつ、その際に直面した課題や考えたことについて、皆さんへ共有できればなと思います。
最後までお読みいただきありがとうございます。
(来月、アドベントカレンダーもある...マ?