🔛

automapper/nestjsを使ってみた

2024/03/20に公開

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