openapi-zod-clientで生成内容をいじる方法[zodios, openapi-zod-client]

プロジェクトでzodiosを採用したのですが、APIの定義がかなり手作業で行うのはめんどくさかったため、swaggerからzodiosのコードを生成させるようにしました。

そこで生成内容を変えたいと思ったので、その方法をご紹介したいと思います。

# そもそもzodiosとは
型バリデーションライブラリであるzodとHTTP通信を簡単に行うためのライブラリであるaxiosを組み合わせたライブラリです。

これを使うことで簡単にAPIを叩くこともでき、エラーハンドリングもライブラリが行ってくれます。

# openapi-zod-client

swagger等のopenAPIから、上記で説明したzodiosのコードを生成できるライブラリです。

https://github.com/astahmer/openapi-zod-client/tree/main

## 通常使用の場合はどうなるのか

swaggerが下のように書かれていたとします。
```generated.gen.swagger.yml
openapi: 3.0.0
info:
  title: User Management API
  description: ユーザ管理のAPI
  version: 1.0.0
paths:
  /users/{uuid}:
    get:
      operationId: getUser
      description: user取得
      parameters:
        - $ref: '#/components/parameters/UserUUID'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
  /users/signup:
    post:
      operationId: UserSignUp
      description: userのsignup
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserSignupRequest'
      responses:
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSignupResponse'
  /users/signin:
    post:
      operationId: UserSignIn
      description: userのsigninリクエスト
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserSignInRequest'
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSignInResponse'
        '404':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSignInError'
components:
  parameters:
    UserUUID:
      name: uuid
      in: path
      description: userのuuid
      required: true
      schema:
        type: string
        format: uuid
      example: 1
  schemas:
    User:
      type: object
      properties:
        uuid:
          type: string
          format: uuid
          description: userのuuid
        name:
          type: string
          format: string
          description: userの名前
        email:
          type: string
          format: string
          description: userのemail
    UserSignupRequest:
      type: object
      properties:
        name:
          type: string
          format: string
          description: userの名前
        email:
          type: string
          format: string
          description: userのemail
        password:
          type: string
          format: string
          description: userのパスワード
      required:
        - name
        - email
        - password
    UserSignupResponse:
      type: object
      properties:
        uuid:
          type: string
          format: string
          description: userのuuid
      required:
        - uuid
    UserSignInRequest:
      type: object
      properties:
        email:
          type: string
          format: string
          description: userのemail
        password:
          type: string
          format: string
          description: userのパスワード
      required:
        - email
        - password
    UserSignInResponse:
      type: object
      properties:
        uuid:
          type: string
          format: string
          description: userのuuid
      required:
        - uuid
    UserSignInError:
      properties:
        errors:
          items:
            example: エラーが発生しました
            type: string
          type: array
      required:
        - code
        - errors
      type: object

```


このファイルからコードを生成するには下のコマンドを使用します。
```zsh:zsh
pnpx openapi-zod-client "./docs/swagger/generated.gen.swagger.yml" -o "./frontend/src/lib/client.ts" --api-client-name usermanageAPI
```

そうすると下のようなファイルが生成されます。
```client.ts
import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";
import { z } from "zod";

const User = z
  .object({ uuid: z.string(), name: z.string(), email: z.string() })
  .partial()
  .passthrough();
const UserSignupRequest = z
  .object({ name: z.string(), email: z.string(), password: z.string() })
  .passthrough();
const UserSignupResponse = z.object({ uuid: z.string() }).passthrough();
const UserSignInRequest = z
  .object({ email: z.string(), password: z.string() })
  .passthrough();
const UserSignInResponse = z.object({ uuid: z.string() }).passthrough();
const UserSignInError = z.object({ errors: z.array(z.string()) }).passthrough();

export const schemas = {
  User,
  UserSignupRequest,
  UserSignupResponse,
  UserSignInRequest,
  UserSignInResponse,
  UserSignInError,
};

const endpoints = makeApi([
  {
    method: "get",
    path: "/users/:uuid",
    alias: "getUser",
    description: `user取得`,
    requestFormat: "json",
    parameters: [
      {
        name: "uuid",
        type: "Path",
        schema: z.number().int(),
      },
    ],
    response: User,
  },
  {
    method: "post",
    path: "/users/signin",
    alias: "UserSignIn",
    description: `userのsigninリクエスト`,
    requestFormat: "json",
    parameters: [
      {
        name: "body",
        type: "Body",
        schema: UserSignInRequest,
      },
    ],
    response: z.object({ uuid: z.string() }).passthrough(),
    errors: [
      {
        status: 404,
        schema: UserSignInError,
      },
    ],
  },
  {
    method: "post",
    path: "/users/signup",
    alias: "UserSignUp",
    description: `userのsignup`,
    requestFormat: "json",
    parameters: [
      {
        name: "body",
        type: "Body",
        schema: UserSignupRequest,
      },
    ],
    response: z.object({ uuid: z.string() }).passthrough(),
  },
]);

export const usermanageAPI = new Zodios(endpoints);

export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
  return new Zodios(baseUrl, endpoints, options);
}

```

# どうしていじる必要があったのか
エラーハンドリングでzodiosのライブラリである`isErrorFromAlias`等を使用する場合に、endpointsを参照する必要がありました。

https://www.zodios.org/docs/client/error

ですが、通常の生成されるzodiosだとexportされていないため、どうにか生成内容をいじる必要がありました。

# 生成内容をいじる
生成内容をいじるにはhbsを書く必要があります。


## hbsをいじる

githubのリポジトリにあったhbsファイルを使用します

https://github.com/astahmer/openapi-zod-client/blob/main/lib/src/templates/default.hbs

:::note
### hbsファイルとは
handlebarsの略で、Handlebarsテンプレートエンジンで使用されるテンプレートファイルのことを指します。

HandlebarsはJavaScriptベースのツールで、HTMLやその他のテキストベースのドキュメントを動的に生成するためのテンプレートファイルです。
:::

元のファイルは下のようになっていました。

```default.hbs
import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";
import { z } from "zod";

{{#if imports}}
{{#each imports}}
import { {{{@key}}} } from "./{{{this}}}"
{{/each}}
{{/if}}


{{#if types}}
{{#each types}}
{{{this}}};
{{/each}}
{{/if}}

{{#each schemas}}
const {{@key}}{{#if (lookup ../emittedType @key)}}: z.ZodType<{{@key}}>{{/if}} = {{{this}}};
{{/each}}

{{#ifNotEmptyObj schemas}}
export const schemas = {
{{#each schemas}}
	{{@key}},
{{/each}}
};
{{/ifNotEmptyObj}}

const endpoints = makeApi([
{{#each endpoints}}
	{
		method: "{{method}}",
		path: "{{path}}",
		{{#if @root.options.withAlias}}
		{{#if alias}}
		alias: "{{alias}}",
		{{/if}}
		{{/if}}
		{{#if description}}
		description: `{{description}}`,
		{{/if}}
		{{#if requestFormat}}
		requestFormat: "{{requestFormat}}",
		{{/if}}
		{{#if parameters}}
		parameters: [
			{{#each parameters}}
			{
				name: "{{name}}",
				{{#if description}}
				description: `{{description}}`,
				{{/if}}
				{{#if type}}
				type: "{{type}}",
				{{/if}}
				schema: {{{schema}}}
			},
			{{/each}}
		],
		{{/if}}
		response: {{{response}}},
		{{#if errors.length}}
		errors: [
			{{#each errors}}
			{
				{{#ifeq status "default" }}
				status: "default",
				{{else}}
				status: {{status}},
				{{/ifeq}}
				{{#if description}}
				description: `{{description}}`,
				{{/if}}
				schema: {{{schema}}}
			},
			{{/each}}
		]
		{{/if}}
	},
{{/each}}
]);

export const {{options.apiClientName}} = new Zodios({{#if options.baseUrl}}"{{options.baseUrl}}", {{/if}}endpoints);

export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
    return new Zodios(baseUrl, endpoints, options);
}
```

上のファイルでexportされていないendpointsを、下のようにhbsファイルを書き換えることでexportさせます。

```template.hbs
import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";
import { z } from "zod";

{{#if imports}}
{{#each imports}}
import { {{{@key}}} } from "./{{{this}}}"
{{/each}}
{{/if}}


{{#if types}}
{{#each types}}
{{{this}}};
{{/each}}
{{/if}}

{{#each schemas}}
const {{@key}}{{#if (lookup ../emittedType @key)}}: z.ZodType<{{@key}}>{{/if}} = {{{this}}};
{{/each}}

{{#ifNotEmptyObj schemas}}
export const schemas = {
{{#each schemas}}
	{{@key}},
{{/each}}
};
{{/ifNotEmptyObj}}

export const endpoints = makeApi([ //ここを書き換える
{{#each endpoints}}
	{
		method: "{{method}}",
		path: "{{path}}",
		{{#if @root.options.withAlias}}
		{{#if alias}}
		alias: "{{alias}}",
		{{/if}}
		{{/if}}
		{{#if description}}
		description: `{{description}}`,
		{{/if}}
		{{#if requestFormat}}
		requestFormat: "{{requestFormat}}",
		{{/if}}
		{{#if parameters}}
		parameters: [
			{{#each parameters}}
			{
				name: "{{name}}",
				{{#if description}}
				description: `{{description}}`,
				{{/if}}
				{{#if type}}
				type: "{{type}}",
				{{/if}}
				schema: {{{schema}}}
			},
			{{/each}}
		],
		{{/if}}
		response: {{{response}}},
		{{#if errors.length}}
		errors: [
			{{#each errors}}
			{
				{{#ifeq status "default" }}
				status: "default",
				{{else}}
				status: {{status}},
				{{/ifeq}}
				{{#if description}}
				description: `{{description}}`,
				{{/if}}
				schema: {{{schema}}}
			},
			{{/each}}
		]
		{{/if}}
	},
{{/each}}
]);

export const {{options.apiClientName}} = new Zodios({{#if options.baseUrl}}"{{options.baseUrl}}", {{/if}}endpoints);

export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
    return new Zodios(baseUrl, endpoints, options);
}
```

## 生成してみる

生成するには、元のコマンドに-tオプションを使ってでテンプレートを指定します。

```zsh:zsh
pnpx openapi-zod-client "./docs/generated.gen.swagger.yml" -o "./client/src/lib/zodios.ts" --api-client-name usermanageAPI -t "./template.hbs"
```

|オプション|機能
|---|---
|-o|アウトプットするファイルを指定
|--api-client-name|APIの名前を指定
|-t|hbsのテンプレートファイルを指定


そうすると下のようにendpointsがexportされます。
```client.ts
import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core";
import { z } from "zod";

const User = z
  .object({ uuid: z.string().uuid(), name: z.string(), email: z.string() })
  .partial()
  .passthrough();
const UserSignupRequest = z
  .object({ name: z.string(), email: z.string(), password: z.string() })
  .passthrough();
const UserSignupResponse = z.object({ uuid: z.string() }).passthrough();
const UserSignInRequest = z
  .object({ email: z.string(), password: z.string() })
  .passthrough();
const UserSignInResponse = z.object({ uuid: z.string() }).passthrough();
const UserSignInError = z.object({ errors: z.array(z.string()) }).passthrough();

export const schemas = {
  User,
  UserSignupRequest,
  UserSignupResponse,
  UserSignInRequest,
  UserSignInResponse,
  UserSignInError,
};

export const endpoints = makeApi([
  {
    method: "get",
    path: "/users/:uuid",
    alias: "getUser",
    description: `user取得`,
    requestFormat: "json",
    parameters: [
      {
        name: "uuid",
        type: "Path",
        schema: z.string().uuid(),
      },
    ],
    response: User,
  },
  {
    method: "post",
    path: "/users/signin",
    alias: "UserSignIn",
    description: `userのsigninリクエスト`,
    requestFormat: "json",
    parameters: [
      {
        name: "body",
        type: "Body",
        schema: UserSignInRequest,
      },
    ],
    response: z.object({ uuid: z.string() }).passthrough(),
    errors: [
      {
        status: 404,
        schema: UserSignInError,
      },
    ],
  },
  {
    method: "post",
    path: "/users/signup",
    alias: "UserSignUp",
    description: `userのsignup`,
    requestFormat: "json",
    parameters: [
      {
        name: "body",
        type: "Body",
        schema: UserSignupRequest,
      },
    ],
    response: z.object({ uuid: z.string() }).passthrough(),
  },
]);

export const usermanageAPI = new Zodios(endpoints);

export function createApiClient(baseUrl: string, options?: ZodiosOptions) {
  return new Zodios(baseUrl, endpoints, options);
}
```

hbsファイルを使えばドメインごとにAPIを分けることもできるかもしれません。

ぜひ活用してみてください！