OpenAPI Generator を使ってNext.js と NestJSを連携する方法

OpenAPI Generator を使ってNext.js と NestJSを連携する方法

Next.jsNestJSTypeScript

おはこんにちばんは。最近、NextJS が出力したOpenAPI (Swagger) によって記述された API 定義ファイルをもとに、Next.js でTypeScript の型や API クライアントを自動生成したいと思ったのですが、意外とまとまった資料がなかったのでその方法をご紹介します。今回は、openapi generator typescript-axios を元に利用します。

サンプルはこちら。(フロントバック

環境

  • Node.js v12.18.0
  • macOS Big Sur

NextJS でAPIを用意

こちらに関しては、前回ご紹介した NestJSにMikroORMの導入手順。CRUDのAPIを実装するまで。 で利用したリポジトリを修正していきます。

パッケージの導入

APIの定義ファイルを出力する必要があるので、Swagger を導入します。

1yarn add @nestjs/swagger swagger-ui-express

次にmain.tsに以下の設定情報を追加します。

./src/main.ts
1async function bootstrap() {
2 const app = await NestFactory.create(AppModule);
3
4 const config = new DocumentBuilder()
5 .setTitle('Example API')
6 .setDescription('The Example API description')
7 .setVersion('1.0')
8 .build();
9
10 const options: SwaggerDocumentOptions = {
11 operationIdFactory: (_: string, methodKey: string) => methodKey,
12 };
13
14 const document = SwaggerModule.createDocument(app, config, options);
15 SwaggerModule.setup('api', app, document);
16
17 await app.listen(8080);
18}

OpenAPI Generatorは API クライアントを生成する際に OperationId を参照しています。一方で nestjs/swagger は OperationIdを「Controller名 + メソッド名」で出力します。

なので、 API クライアントでアクセスする際、呼び出しがexampleApi.controller.method となり少しくどくなります。そこでexampleApi.methodとするために以下を行なっています。

1const options: SwaggerDocumentOptions = {
2 operationIdFactory: (_: string, methodKey: string) => methodKey,
3};

最後にコントローラー毎にインスタンスを区切るためにタグを以下のように追加しましょう。

./src/example/example.controller.ts
1@ApiTags('example')
2export class ExampleController {
3...
4

これでNestJS側の最低限の設定は以上になります。

Next.jsにopenapi generator typescript-axios を導入

次にNext.js側の設定を行なっていきます。

プロジェクトを作成

1yarn create next-app --typescript

パッケージを導入

1yarn add -D openapitools/openapi-generator-cli
2
3yarn add axios gulp

openapi generator typescript-axios の設定

以下の設定ファイルを追加します。

./openapiConfig.yml
1withSeparateModelsAndApi: true
2apiPackage: 'api'
3modelPackage: 'dto'
4modelPropertyNaming: 'camelCase'
5supportsES6: true
6withInterfaces: true

  • withSeparateModelsAndApi
  • API と model を別々にしたかったので true にしています。
  • apiPackage
  • api が格納されるフォルダ名を api としたかったので api にしています。
  • modelPackage
  • model が格納されるフォルダ名を model としたかったので model にしています。
  • modelPropertyNaming
  • キャメルケースで出力したかったためです。
  • supportsES6
  • withInterfaces
  • 使ってもいいかなっと思ったので追加しています。(雑)

出力するコマンドを追加

APIエンドポイントの TypeScript の型 と API クライアント 自動生成するコマンドを追加します。

openapi generator は openapi-generator-cli generate -g typescript-axios -i <OpenAPI定義ファイル> -o <出力先>のようなコマンドで出力します。ただ、OpenAPI定義ファイルの出力先が本番や開発環境で変わる可能性があるので、gulp を利用して環境変数から出力先を変えられるようにします。gulpの使い方については省略します。

以下のファイルを追加します。

./gulpfile.ts
1import * as cp from "child_process";
2
3import { loadEnvConfig } from "@next/env";
4import gulp from "gulp";
5
6loadEnvConfig(process.env.PWD ?? "");
7
8gulp.task("generate-example-client", (done) => {
9 cp.spawnSync(
10 "openapi-generator-cli",
11 [
12 "generate",
13 "-g",
14 "typescript-axios",
15 "-i",
16 `${process.env.API_JSON_URL ?? ""}`,
17 "-o",
18 "./openapi-generator/example-api",
19 "-c",
20 "./openapiConfig.yml",
21 ],
22 {
23 stdio: [process.stdin, process.stdout, process.stderr],
24 shell: true,
25 }
26 );
27 done();
28});
29

環境変数には以下を追加します。

./.env
1API_JSON_URL=http://localhost:8080/api-json

最後にコマンドを追加します。

./package.json
1"scripts": {
2 "generate-example-client": "gulp generate-example-client"
3},
4....

openapi generator typescript-axios で出力

先ほど追加したコマンドを実行します。

1yarn generate-example-client

以下が出力されれば完了です。

1################################################################################
2# Thanks for using OpenAPI Generator.                                          #
3# Please consider donation to help us maintain this project 🙏                 #
4# https://opencollective.com/openapi_generator/donate                          #
5################################################################################
6Finished 'generate-example-client' after 3.55 s

APIクライアント まとめるクラスを追加

エンドポイント毎に設定を渡す必要があるので、集約するクラスを定義しておきます。basePathにはバックエンド側のURLを指定します。

./clients/ExampleClient.ts
1import { Configuration, ExampleApi } from "../openapi-generator/example-api";
2
3export class ExampleClient {
4 private config = new Configuration({
5 basePath: process.env.NEXT_PUBLIC_API_BASE_URL || "/api",
6 });
7
8 public exampleApi = new ExampleApi(this.config);
9}

インスタンスをどこでも扱えるようにする

最後に ExampleClient をどこでも扱えるようにします。今回は context を利用します。ここでは細かい説明は行いません。

./contexts/ExampleClientContext.ts
1import React from "react";
2
3import type { ExampleClient } from "../clients/ExampleClient";
4
5export const ExampleClientContext = React.createContext<
6 ExampleClient | undefined
7>(undefined);

./pages/_app.tsx
1const exampleClient = new ExampleClient();
2
3function MyApp({ Component, pageProps }: AppProps) {
4 return (
5 <ExampleClientContext.Provider value={exampleClient}>
6 <Component {...pageProps} />
7 </ExampleClientContext.Provider>
8 );
9}

./hooks/useExampleClient.ts
1import { useContext } from "react";
2
3import type { ExampleClient } from "../clients/ExampleClient";
4import { ExampleClientContext } from "../contexts";
5
6export const useExampleClient = (): ExampleClient => {
7 const exampleClient = useContext(ExampleClientContext);
8 if (!exampleClient)
9 throw new Error("useExampleClient must be inside a Provider with a value");
10
11 return exampleClient;
12};

以下のようAPIをアクセスできるようになれば完了です。

1useEffect(() => {
2 void (async () => {
3 console.log(await exampleClient.exampleApi.findAll());
4 })();
5});

これで終わりです。お疲れ様でした。

最後に

OpenAPI Generator typescript-axios を使ってNext.js と NestJSを連携する方法をご紹介しました。TypeScript の型や API クライアントを自動生成されるので便利ですね。ただ、ドキュメントなどはあまり揃ってないので、利用するにはある程度の覚悟が必要かなっと思います。