4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

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

そもそもzodiosとは

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

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

openapi-zod-client

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

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

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
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を参照する必要がありました。

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

生成内容をいじる

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

hbsをいじる

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

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
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を分けることもできるかもしれません。

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?