NestJSのDTOをフロントでも使って型を使いまわす勘所(Swagger)
勝手に1人アドカレの6日目です。5日目はこちら
NestJSでリクエストのバリデーションをするとき
NestJSにおいて、DTOを作っておくことで、リクエストのバリデーションや、リクエストの整形などをすることができます。
DTOについてはこの記事で理解できるかと思います。
公式ドキュメントはこちら
アーキテクチャ
フロント: React/Next.js(v14.0.4)
API:NestJS(v18.0.6) + Swagger
モノレポで開発していて、管理ツールにはnx(v18.0.6)を使用してます。
フロントからのリクエストの型とAPIで受け取る型を共通化
弊社のサービスのコードを例に挙げます。
フロントの実装
例えばフロントではこんなログインページがあったとします。
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
を参照しています。こうすることでデコレータに則ったサーバーサイドバリデーションが可能になります。
@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できるようにしています。
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.rules
にloader
を読み込ませます。
loaderの実装
// 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を適用
// 省略
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
絶賛エンジニア募集中!
ご興味ある方は僕とカジュアル面談しましょう!!!
Discussion