⚠️

NestJSのDTOをフロントでも使って型を使いまわす勘所(Swagger)

2024/12/07に公開

勝手に1人アドカレの6日目です。5日目はこちら

NestJSでリクエストのバリデーションをするとき

NestJSにおいて、DTOを作っておくことで、リクエストのバリデーションや、リクエストの整形などをすることができます。

DTOについてはこの記事で理解できるかと思います。
https://zenn.dev/minateru/articles/afc74c519461a9

公式ドキュメントはこちら
https://docs.nestjs.com/techniques/validation#auto-validation

アーキテクチャ

フロント: React/Next.js(v14.0.4)
API:NestJS(v18.0.6) + Swagger
モノレポで開発していて、管理ツールにはnx(v18.0.6)を使用してます。

フロントからのリクエストの型とAPIで受け取る型を共通化

弊社のサービスのコードを例に挙げます。

フロントの実装

例えばフロントではこんなログインページがあったとします。

login.tsx
import { ERRORS, LoginRequest } from '@hccloud/types';

export const LoginContainer = () => {
  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm<LoginRequest>({
    resolver: classValidatorResolver(LoginRequest),
  });

  const onSubmit: SubmitHandler<LoginRequest> = async (data) => {
    // ログインリクエスト処理
  };

  return <LoginComponent {...{ onSubmit, errors, register }} />;
};

classValidatorResolverの引数にはDTOを渡しています。フロントサイドバリデーションに使ってます。

NestJS側(API)実装

一方のリクエストを受け取る側(NestJS)のauth.controller.tsのログイン関数
ここでもフロントと同じLoginRequestを参照しています。こうすることでデコレータに則ったサーバーサイドバリデーションが可能になります。

auth.controller
  @Post('login')
  @ApiOperation({ summary: 'ログイン', description: 'ログイン' })
  @ApiResponse({
    description: '成功時は資格情報を持ったJWTを返却',
    type: CognitoUserSession,
  })
  async login(@Body() authenticateRequest: LoginRequest): Promise<CognitoUserSession> {
    try {
      const result = await this.authService.login(authenticateRequest);
      return result;
    } catch (e) {
      // エラー時の処理(ここでは省略)
    }
  }

DTOの実装

これがDTOの本体。nxのlibsに作ることでフロント、APIの双方からimportできるようにしています。

LoginRequest.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';

export class LoginRequest {
  @ApiProperty({
    description: 'メールアドレス',
    type: String,
  })
  @IsString()
  name: string;

  @ApiProperty({
    description: 'パスワード',
    type: String,
  })
  @IsString()
  password: string;
}

問題

ここまではわりと普通に行われている設計だと思います(そうだよね…?)

しかしここで問題が。

DTOではAPIドキュメントのために@nestjs/swaggerを使用しています。これがフロントのビルド時になんらかのモジュールが足りないなどで落ちてしまいます(SwaggerはAPIサーバーでホスティングしますからね)

具体的にはこんな感じのエラー

   ▲ Next.js 14.0.4
   - Local:        http://localhost:3000

 ✓ Ready in 2.5s
 ✓ Compiled /middleware in 208ms (64 modules)
 ○ Compiling /404 ...
 ⨯ ../../node_modules/@nestjs/core/injector/injector.js:9:0
Module not found: Can't resolve 'perf_hooks'

https://nextjs.org/docs/messages/module-not-found

Import trace for requested module:
../../node_modules/@nestjs/core/nest-application.js
../../node_modules/@nestjs/core/index.js
../../node_modules/@nestjs/swagger/dist/swagger-explorer.js
../../node_modules/@nestjs/swagger/dist/swagger-scanner.js
../../node_modules/@nestjs/swagger/dist/swagger-module.js
../../node_modules/@nestjs/swagger/dist/index.js
../../node_modules/@nestjs/swagger/index.js

解決方法

要はフロントのビルド時に@nestjs/swaggerのデコレーターをスキップできれば解決します。

webpackのconfig.module.rulesloaderを読み込ませます。

loaderの実装

dto-adapter.loader.js
// NOTE: NestJSのモジュールを使用するSwaggerデコレーターをフロントでのコンパイル時に削除するためのローダー
// https://stackoverflow.com/questions/74939029/how-to-use-nx-dto-libraries-decorated-with-nestjs-swagger-api-in-frontend-framew
module.exports = (source) => {
  const decoratorsToRemove = [
      /@ApiProperty\(\{[^}]*\}\)\s*/g,
      /@ApiPropertyOptional\(\{[^}]*\}\)\s*/g,
      /@ApiExtraModels\([^)]*\)\s*/g, // @ApiExtraModels(...) の削除
      // 他のデコレーターを追加する場合はここに追記
  ];

  let result = source;
  decoratorsToRemove.forEach((regex) => {
      result = result.replace(regex, '');
  });

  return result;
};

loaderを適用

next.config.js
// 省略

const nextConfig = {
  webpack: (
    config,
    { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack }
  ) => {
    // ここ👇
    config.module.rules.push({
      test: /\.ts$/,
      loader: path.resolve(__dirname, './webpack/loaders/dto-adapter.loader.js'),
      exclude: /node_modules/,
    });
    return config
  },
}

// 省略

参考にしたStackOverFlow

https://stackoverflow.com/questions/74939029/how-to-use-nx-dto-libraries-decorated-with-nestjs-swagger-api-in-frontend-frame

絶賛エンジニア募集中!

ご興味ある方は僕とカジュアル面談しましょう!!!
https://arwrk.net/recruit/yaccdmcf2smnstb/2518879/

https://meetings-eu1.hubspot.com/suyama-daichi

Discussion