automapper/nestjsを使ってみた
automapper/nestjsとは
automapper/nestjsはオブジェクトからオブジェクトへの値の詰め替えをほぼ自動で行ってくれるNode.jsのパッケージです。
今回、BFF(Backend For Frontend)を開発するにあたり、複数のバックエンドからのレスポンスを整理してフロントエンドに返したかったので、導入しました。
特徴
オブジェクトからオブジェクトへのマッピングに特化しており、シンプルにプロジェクトに導入することができます。
また、ドキュメントが充実しており、やりたいことを実現しやすいです。
パッケージの種類
- @automapper/core
automapperのコアとなるパッケージです。automapperを使う場合はインストール必須です。 - @automapper/classes
TS/ES6のクラスをマッピングする際に利用するパッケージです。この記事ではこのパッケージに焦点を当てます - @automapper/pojos
インターフェイス/タイプとPOJOのマッピングの際に利用するパッケージです。 - @automapper/nestjs
automapperとNestJSを統合するためパッケージです。automapperのマッピング定義をDIすることができるようになります。
準備
インストール
次の3つのパッケージをインストールします。
npm i @automapper/core
npm i @automapper/classes
npm i @automapper/nestjs
AppModuleへのインポート
app.module.ts
にインポートを追加します。次の例は、クラス間のマッピングを行う場合の記載です。その他の例や詳細は公式ドキュメントを参照してください。
@Module({
imports: [
AutomapperModule.forRoot({
strategyInitializer: classes(),
}),
],
providers: [TestProfile], // 後述のProfileクラス
})
export class AppModule {}
実装
Profileの作成
マッピングはAutomapperProfile
クラスを継承したProfileクラスに定義します。
以下にProfileクラスの実装例を示します。
@Injectable()
export class TestProfile extends AutomapperProfile {
constructor(@InjectMapper() mapper: Mapper) {
super(mapper);
}
override get profile() {
return (mapper) => {
// SourceDtoからDestinationDtoへのマッピングを定義する
createMap(mapper, SourceDto, DestinationDto,
forMember(
(d) => d.name,
mapFrom((s) => s.lastName + " " + s.firstName)
),
// オブジェクトのマッピングも可能
// ただし、異なるクラス間のマッピングは、createMapによるマッピング定義が必要
// (後述のSourceChildDtoからDestinationChildDtoへのマッピング定義)
forMember(
(d) => d.child,
mapWith(DestinationChildDto, SourceChildDto, (s) => s.child),
),
forMember(
(d) => d.sex,
mapFrom((s) => sexMap.get(s.sex)),
)
);
// SourceChildDtoからDestinationChildDtoへのマッピングを定義する
createMap(mapper, SourceChildDto, DestinationChildDto,
// 型変換も可能(例: string => boolean)
typeConverter(String, Boolean, (s) => s === 'true'),);
}
}
}
マッピング定義の解説
automapperは、基本的には名前が同じプロパティ同士を自動的にマッピングします。
ただ、プロパティ名が違ったり、型が違ったりするケースは、手動でマッピングを定義してあげる必要があります。
手動でのマッピングは、forMember
関数を使います。
複数⇒単数のマッピング
複数のプロパティを組み合わせて一つのプロパティにマッピングするには、次のように指定します。
姓と名を、姓+" "+名=氏名
としてマッピングしています。
forMember(
(d) => d.name,
mapFrom((s) => s.lastName + " " + s.firstName)
),
オブジェクトのマッピング
オブジェクトの中にオブジェクトが含まれる場合も、mapWith
を使ってマッピングを定義することができます。
forMember(
(d) => d.child,
mapWith(DestinationChildDto, SourceChildDto, (s) => s.child),
),
そして、オブジェクトの中のオブジェクト同士のマッピングも、createMap
で定義することができます。
下の「型変換」で紹介しているcreateMap
がそれに当たります。
型変換
typeConverter
を利用することで、型変換のルールを定義することができます。
下の例では、"true"という文字列をbooleanのtrue
に変換しています。("true"以外はすべてfalse
に変換されます)
createMap(mapper, SourceChildDto, DestinationChildDto,
// 型変換も可能(isActive: string => boolean)
typeConverter(String, Boolean, (s) => s === 'true'),);
コードのマッピング
例えば、古いシステムで"1"(男性)
というコードが使われており、BFFでそのコードをmale
という定数に変換したい場合は、javascriptのMap
オブジェクトを使うことで変換ルールを作ることができます。
forMember(
(d) => d.sex,
mapFrom((s) => sexMap.get(s.sex)),
)
sexMapは次のように定義します。
export const sexMap = new Map<String, Sex>([
["1", Sex.MALE],
["2", Sex.FEMALE],
["3", Sex.OTHER],
]);
DTOの変換方法
作成したマッピング定義を使ってDTOを変換するには、次のように記載します。
まず、コンストラクターでマッピング定義をMapper
クラスのオブジェクトmapper
にDIします。
constructor(
@InjectMapper() private readonly mapper: Mapper,
) { }
そして、そのmapper
を使って、DTOを変換します。
const dst: DestinationDto = this.mapper.map(src, SourceDto, DestinationDto);
このmapper
は@InjectMapper()
デコレータを使ってDIすることで、NestJSプロジェクト内のどこでも使うことができます。
おわりに
既存のバックエンドを活用して、新たに開発するモダンなフロントエンドに情報を提供する場合など、BFFを開発するケースがあると思います。
レスポンスを意図した形に変換する場合に便利なパッケージなので、この記事を参考に、ぜひ活用いただければと思います!
Discussion