Closed7

Next.js + OpenAPI3-TS + Zodで、APIドキュメント(swagger)をメンテナンスフリーにする

Ryuki SasakiRyuki Sasaki

自分の基礎能力を向上させるのが目的のため、ChatGPTやCursorは極力使用禁止とする。

スクラップの目的

現状

  • Next.jsでAPIを開発している
  • 開発速度とトレードオフに、ドキュメントがメンテナンスされていない問題がある。

やりたいこと

  • OpenAPI3-TSを使用することで、APIのドキュメントが自動生成出来るか試す。
  • Zod to OpenAPIを使用することで、APIのinputを制御出来るか試す。

各種ドキュメント

OpenAPI3-TS

https://github.com/metadevpro/openapi3-ts

Zod to Open API

https://github.com/asteasolutions/zod-to-openapi

Ryuki SasakiRyuki Sasaki

環境

npx create-next-app@latest --typescriptでNext.jsのプロジェクトを作成。
latestはv16.0.0でした。

  • Next.js:v16.0.0
  • Node.js:v24.x

OpenAPI3-TSとZod to Open APIとZodをインストール

npm i openapi3-ts @asteasolutions/zod-to-openapi zodを実行

{
"name": "example-auto-generate-openapi3-docs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^8.1.0",
"next": "16.0.0",
"openapi3-ts": "^4.5.0",
"react": "19.2.0",
"react-dom": "19.2.0",
"zod": "^4.1.12"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.0.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}

Ryuki SasakiRyuki Sasaki

ZodにOpenAPIを拡張

  • Zod to Open APIの公式ドキュメントになるべく従いながら作成していく。
  • extendZodWithOpenApiを使用してZodにopenapiを付けれるようにする。
  • この.openapiがschemaのExampleコードになるみたい。
src/schemas/user-schema.ts
import {extendZodWithOpenApi} from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);

const TaskSchema = z
.object({
  id: z.string().openapi({ example: '1212121' }),
  name: z.string().openapi({ example: 'Example Task' }),
  completed: z.boolean().openapi({ example: false }),
})
.openapi('Task');
Ryuki SasakiRyuki Sasaki

Example Codeを見ながら進める

https://github.com/asteasolutions/zod-to-openapi/blob/master/example/index.ts

OpenAPI用のドキュメントを書く

  • new OpenAPIRegistry();を使用して、OpenAPIの生成に必要な各種情報を登録する。
  • GETとPOSTを用意した。
src/schemas/todo-schema.ts
import {extendZodWithOpenApi, OpenAPIRegistry} from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';

extendZodWithOpenApi(z);
export const registry = new OpenAPIRegistry();

/*
  - GET /api/tasks - タスク取得
 */
const TaskResponseSchema = z
.object({
  id: z.string().openapi({ example: '1212121' }),
  name: z.string().openapi({ example: 'Example Task' }),
  completed: z.boolean().openapi({ example: false }),
})
.openapi('Task');

registry.registerPath({
  method: "get",
  path: "/api/tasks",
  tags: ["task"],
  summary: "タスク取得",
  description: "登録されているタスクを返却します",
  request: {},
  responses: {
    200: {
      description: "タスクの取得成功",
      content: {
        "application/json": {
          schema: TaskResponseSchema,
        },
      },
    },
  },
});

/*
 - POST /api/tasks - タスク登録
 */
const CreateTaskSchema = z
.object({
  name: z.string().openapi({ example: 'Example Task' }),
}).openapi('Task');

registry.registerPath({
  method: "post",
  path: "/api/tasks",
  tags: ["task"],
  summary: "タスク登録",
  description: "タスクを登録します",
  request: {
    body: {
      content: {
        "application/json": {
          schema: CreateTaskSchema,
        },
      },
    },
  },
  responses: {
    200: {
      description: "タスクの取得成功",
      content: {
        "application/json": {
          schema: TaskResponseSchema,
        },
      },
    },
  },
});
Ryuki SasakiRyuki Sasaki

OpenAPIのYamlが出力されるようにする

getOpenApiDocumentationwriteDocumentationを設定する
ExampleCodeは__dirnameを使用しているが、ESMでは使えないためprocess.cwd()を使用する。

import {extendZodWithOpenApi, OpenApiGeneratorV3, OpenAPIRegistry} from '@asteasolutions/zod-to-openapi';
import { z } from 'zod';
import * as yaml from 'yaml';
import * as fs from 'fs';
import path from "node:path";

extendZodWithOpenApi(z);

export const registry = new OpenAPIRegistry();

...

function getOpenApiDocumentation() {
  const generator = new OpenApiGeneratorV3(registry.definitions);

  return generator.generateDocument({
    openapi: '3.0.0',
    info: {
      version: '1.0.0',
      title: 'My API',
      description: 'This is the API',
    },
    servers: [{ url: 'v1' }],
  });
}

function writeDocumentation() {
  // OpenAPI JSON
  const docs = getOpenApiDocumentation();

  // YAML equivalent
  const fileContent = yaml.stringify(docs);

  const outPath = path.resolve(process.cwd(), "public/openapi-docs.yml");

  fs.writeFileSync(outPath, fileContent, {
    encoding: 'utf-8',
  });
}

writeDocumentation();
Ryuki SasakiRyuki Sasaki

package.jsonにスクリプトを登録

  • package.jsongenerate-docsのスクリプトを登録
package.json
{
  "name": "example-auto-generate-openapi3-docs",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "generate-docs": "node src/schemas/task-schema.ts",
    "lint": "eslint"
  },
 ...
}

npm run generate-docsを実行

  • 出力の確認
出力
openapi: 3.0.0
info:
  version: 1.0.0
  title: My API
  description: This is the API
servers:
  - url: v1
components:
  schemas:
    Task:
      type: object
      properties:
        id:
          type: string
          example: "1212121"
        name:
          type: string
          example: Example Task
        completed:
          type: boolean
          example: false
      required:
        - id
        - name
        - completed
  parameters: {}
paths:
  /api/tasks:
    get:
      tags:
        - task
      summary: タスク取得
      description: 登録されているタスクを返却します
      responses:
        "200":
          description: タスクの取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
    post:
      tags:
        - task
      summary: タスク登録
      description: タスクを登録します
      requestBody:
        content:
          application/json:
            schema:
              allOf:
                - $ref: "#/components/schemas/Task"
                - properties:
                    name:
                      type: string
                      example: Example Task
                  required:
                    - name
      responses:
        "200":
          description: タスクの取得成功
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
このスクラップは2日前にクローズされました