おはこんにちばんは。最近、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
に以下の設定情報を追加します。
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 の設定
以下の設定ファイルを追加します。
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の使い方については省略します。
以下のファイルを追加します。
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
環境変数には以下を追加します。
1API_JSON_URL=http://localhost:8080/api-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);
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 クライアントを自動生成されるので便利ですね。ただ、ドキュメントなどはあまり揃ってないので、利用するにはある程度の覚悟が必要かなっと思います。