プロジェクトでzodiosを採用したのですが、APIの定義がかなり手作業で行うのはめんどくさかったため、swaggerからzodiosのコードを生成させるようにしました。
そこで生成内容を変えたいと思ったので、その方法をご紹介したいと思います。
そもそもzodiosとは
型バリデーションライブラリであるzodとHTTP通信を簡単に行うためのライブラリであるaxiosを組み合わせたライブラリです。
これを使うことで簡単にAPIを叩くこともでき、エラーハンドリングもライブラリが行ってくれます。
openapi-zod-client
swagger等のopenAPIから、上記で説明したzodiosのコードを生成できるライブラリです。
通常使用の場合はどうなるのか
swaggerが下のように書かれていたとします。
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
このファイルからコードを生成するには下のコマンドを使用します。
pnpx openapi-zod-client "./docs/swagger/generated.gen.swagger.yml" -o "./frontend/src/lib/client.ts" --api-client-name usermanageAPI
そうすると下のようなファイルが生成されます。
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ファイルを使用します
元のファイルは下のようになっていました。
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させます。
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オプションを使ってでテンプレートを指定します。
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されます。
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を分けることもできるかもしれません。
ぜひ活用してみてください!