ブログトップ画像

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

フロントエンド

おはこんにちばんは。最近、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 を導入します。

yarn add @nestjs/swagger swagger-ui-express


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

./src/main.ts

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('Example API')
    .setDescription('The Example API description')
    .setVersion('1.0')
    .build();

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

  const document = SwaggerModule.createDocument(app, config, options);
  SwaggerModule.setup('api', app, document);

  await app.listen(8080);
}


OpenAPI Generatorは API クライアントを生成する際に OperationId を参照しています。一方で nestjs/swagger は OperationIdを「Controller名 + メソッド名」で出力します。
なので、 API クライアントでアクセスする際、呼び出しがexampleApi.controller.method となり少しくどくなります。そこでexampleApi.methodするために以下を行なっています。

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


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

./src/example/example.controller.ts

@ApiTags('example')
export class ExampleController {
...


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

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

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

プロジェクトを作成

yarn create next-app --typescript


パッケージを導入

yarn add -D openapitools/openapi-generator-cli

yarn add axios gulp


openapi generator typescript-axios の設定

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

./openapiConfig.yml

withSeparateModelsAndApi: true
apiPackage: 'api'
modelPackage: 'dto'
modelPropertyNaming: 'camelCase'
supportsES6: true
withInterfaces: 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
import * as cp from "child_process";

import { loadEnvConfig } from "@next/env";
import gulp from "gulp";

loadEnvConfig(process.env.PWD ?? "");

gulp.task("generate-example-client", (done) => {
  cp.spawnSync(
    "openapi-generator-cli",
    [
      "generate",
      "-g",
      "typescript-axios",
      "-i",
      `${process.env.API_JSON_URL ?? ""}`,
      "-o",
      "./openapi-generator/example-api",
      "-c",
      "./openapiConfig.yml",
    ],
    {
      stdio: [process.stdin, process.stdout, process.stderr],
      shell: true,
    }
  );
  done();
});


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

./.env

API_JSON_URL=http://localhost:8080/api-json


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

./package.json
"scripts": {
  "generate-example-client": "gulp generate-example-client"
},
....


openapi generator typescript-axios で出力

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

yarn generate-example-client


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

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


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

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

./clients/ExampleClient.ts

import { Configuration, ExampleApi } from "../openapi-generator/example-api";

export class ExampleClient {
  private config = new Configuration({
    basePath: process.env.NEXT_PUBLIC_API_BASE_URL || "/api",
  });

  public exampleApi = new ExampleApi(this.config);
}


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

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

./contexts/ExampleClientContext.ts

import React from "react";

import type { ExampleClient } from "../clients/ExampleClient";

export const ExampleClientContext = React.createContext<
  ExampleClient | undefined
>(undefined);


./pages/_app.tsx

const exampleClient = new ExampleClient();

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ExampleClientContext.Provider value={exampleClient}>
      <Component {...pageProps} />
    </ExampleClientContext.Provider>
  );
}


./hooks/useExampleClient.ts

import { useContext } from "react";

import type { ExampleClient } from "../clients/ExampleClient";
import { ExampleClientContext } from "../contexts";

export const useExampleClient = (): ExampleClient => {
  const exampleClient = useContext(ExampleClientContext);
  if (!exampleClient)
    throw new Error("useExampleClient must be inside a Provider with a value");

  return exampleClient;
};


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

  useEffect(() => {
    void (async () => {
      console.log(await exampleClient.exampleApi.findAll());
    })();
  });


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

最後に

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