2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

APIを設計・開発するとき、仕様書をどうやって作るか悩んだことはありませんか?
ExcelやWordで書くのも一つの方法ですが、変更が発生したときのメンテナンスが大変だったり、開発者間・チーム間での共有がうまくいかなかったりと、課題が多いです。

そこでおすすめしたいのが OpenAPI です。
OpenAPI を使うことで、効率的かつ統一感のあるAPI仕様書を作成できるだけでなく、その仕様書からコードやドキュメントを自動生成することも可能です。

本記事では OpenAPI についてと、開発での活用方法を紹介します。

OpenAPI とは?

OpenAPI (OpenAPI Specification) は、APIの仕様を記述するためのフォーマットです。
YAMLやJSON形式で記述され、APIのエンドポイントやリクエスト/レスポンスの構造を詳細に定義できます。
以前は、Swagger Specification として知られていました。

特徴としては以下のようなものがあります。

  • 言語に依存しない
    • 実装言語を問わず使える
  • ツールとの連携が豊富
    • Swagger UI で簡単にモックサーバーが建てられる
    • Postman を使って簡単にAPIテストが実施できる
    • コードジェネレーターも豊富で様々な言語のコードを自動生成できる
  • 変更管理が容易
    • テキスト形式なので、Gitでバージョン管理ができる

サンプル

サンプルコードを見ていきたいと思います。
細かい部分の説明はここでは省きます。
「こんな感じで書くのかぁ」とイメージだけ伝われば幸いです。

エンドポイントの定義

メインとなるファイルです。
ユーザー情報に対する CRUD + リスト取得 を定義しています。

# ./openapi.yaml

openapi: 3.1.1
info:
  title: Sample API
  description: API定義サンプル
  version: 0.0.1

security: []

servers:
  - url: http://localhost:8080/api
    description: ローカル環境
paths:
  /users:
    get:
      tags:
        - ユーザー
      summary: ユーザー情報リスト取得
      description: ユーザー情報の一覧を取得する。
      operationId: listUser
      responses:
        "200":
          description: ユーザー情報リスト
          content:
            application/json:
              schema:
                properties:
                  users:
                    type: array
                    items:
                      $ref: "schemas/User.yaml"
                type: object
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    post:
      tags:
        - ユーザー
      summary: ユーザー情報登録
      description: ユーザー情報を登録する。
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: "schemas/User.yaml"
                - required:
                    - email
      responses:
        "201":
          description: ユーザー情報
          content:
            application/json:
              schema:
                $ref: "schemas/User.yaml"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
  /users/{id}:
    get:
      tags:
        - ユーザー
      summary: ユーザー情報取得
      description: 指定されたユーザー情報を取得する。
      operationId: getUser
      parameters:
        - name: id
          in: path
          description: ユーザーID。自身の情報を取得したい場合は`self`を指定する。
          required: true
          schema:
            type: string
            format: uuid
            example: ec00689e-3998-4df0-ae3e-416b052fafbd
      responses:
        "200":
          description: ユーザー情報
          content:
            application/json:
              schema:
                $ref: "schemas/User.yaml"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
    delete:
      tags:
        - ユーザー
      summary: ユーザー情報削除
      description: 指定されたユーザー情報を削除する。
      operationId: deleteUser
      parameters:
        - name: id
          in: path
          description: ユーザーID
          required: true
          schema:
            type: string
            format: uuid
          example: c80f097d-2033-f09f-46d3-54ce1a559f83
      responses:
        "200":
          $ref: "#/components/responses/OK"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags:
        - ユーザー
      summary: ユーザー情報更新
      description: |
        指定されたユーザー情報を更新する。

        ※リクエストボディで指定されていない属性の値は更新しない。
      operationId: updateUser
      parameters:
        - name: id
          in: path
          description: ユーザーID
          required: true
          schema:
            type: string
            format: uuid
          example: c80f097d-2033-f09f-46d3-54ce1a559f83
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "schemas/User.yaml"
      responses:
        "200":
          $ref: "#/components/responses/OK"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"

components:
  responses:
    OK:
      description: 成功
    Unauthorized:
      description: 認証失敗
    NotFound:
      description: 見つからない
    UnprocessableEntity:
      description: バリデーションエラー

モデル・enum定義

エンドポイントの定義から参照されるモデルやenumを定義しています。
これらのファイルは別ファイルに定義することが可能です。
(必要であれば、エンドポイントの定義自体も分割可能)

# ./schemas/User.yaml
title: ユーザー情報
type: object
properties:
  id:
    description: ユーザーID
    type: string
    format: uuid
    readOnly: true
    example: ec00689e-3998-4df0-ae3e-416b052fafbd
  lastName:
    description: 
    type: string
    example: 山田
  firstName:
    description: 
    type: string
    example: 太郎
  tel:
    description: 電話番号
    type: string
    example: 090-1234-5678
  email:
    description: メールアドレス
    type: string
    example: test@mail.address.com
  status:
    $ref: "./enums/UserStatus.yaml"
  password:
    description: パスワード
    type: string
    writeOnly: true
    example: password
  createdAt:
    description: 作成日時
    type: string
    format: date-time
    readOnly: true
    example: "2023-01-01T00:00:00+09:00"
  updatedAt:
    description: 更新日時
    type: string
    format: date-time
    readOnly: true
    example: "2023-01-01T00:00:00+09:00"
title: ユーザーステータス
type: string
description: |
  ステータス。
  - `valid` - 有効
  - `invalid` - 無効
  - `deleted` - 退会済み
enum: ["valid", "invalid", "deleted"]
x-enum-varnames:
  [
    "VALID",
    "INVALID",
    "DELETED",
  ]
example: valid

ドキュメント生成のCI

OpenAPI の仕様書を Redocly というツールを使ってドキュメント化し、GitHub Pages にアップロードする GitHub Actions を定義しています。

# ./.github/workflows/redoc.yaml

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      # see https://github.com/actions/setup-node
      - name: node setting
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}

      - name: install redoc
        run: npm install -g @redocly/cli

      - name: build open api
        run: |
          redocly build-docs ./openapi.yaml
          mkdir ./public
          mv ./redoc-static.html ./public/index.html

      - name: Setup Pages
        uses: actions/configure-pages@v5

      - name: Upload Artifacts to GitHub Pages
        uses: actions/upload-pages-artifact@v3
        with:
          path: ./public

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

mainへのPush/PR作成でPagesを上書きしています。
実際に運用する際は、PR時は「Artifactsにアップロードするだけにする」など工夫が必要です。

生成されたドキュメント

実際にCIで生成したドキュメントが以下です。

image.png

如何でしょうか? とても見やすいドキュメントになっていますね。

OpenAPI からのコード生成

OpenAPI で API仕様を作成するメリットの1つとしてコードの自動生成があります。
例えば、API IF仕様からバックエンド/フロントエンド双方のアプリを開発する場合、仕様書をExcel等で作っていると、それに合わせたモデル定義を作成するのは、大変かつ、単純作業なので苦痛を伴います(笑)
また、変更があった場合に漏れなくメンテナンスしていくのは、長期的に見るとかなりのコストが掛かります。

それを、OpenAPI からの自動生成を利用することで、以下のように OpenAPI の変更をトリガーとして、バックエンドやフロントエンドのモデルの定義を自動生成するCIを組むことができます。

例えば、以下のようなCIを組めば、バックエンドとフロントエンドアプリからはpublish されたライブラリを参照すれば、最新のモデル定義を利用することができます。

サーバーやリクエスト送信のコードを自動生成することも可能ですが、生成されたコードをそのまま利用するのは難しいです。
自動生成はモデルやenum定義のみに留めておくのをオススメします。

自動生成例

OpenAPI Generator というツールを使って、TypeScript のモデル定義を生成する例です。

# ツールのインストール
brew install openapi-generator

# コード生成
openapi-generator generate -i openapi.yaml -g typescript-axios -o dist --global-property models,supportingFiles
生成されたファイル (`api.ts` のみ抜粋)
// ./dist/api.ts

/* tslint:disable */
/* eslint-disable */
/**
 * Sample API
 * API定義サンプル
 *
 * The version of the OpenAPI document: 0.0.1
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
 */


import type { Configuration } from './configuration';
import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios';
import globalAxios from 'axios';
// Some imports not used depending on template conditions
// @ts-ignore
import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from './common';
import type { RequestArgs } from './base';
// @ts-ignore
import { BASE_PATH, COLLECTION_FORMATS, BaseAPI, RequiredError, operationServerMap } from './base';

/**
 * 
 * @export
 * @interface CreateUserRequest
 */
export interface CreateUserRequest {
    /**
     * ユーザーID
     * @type {string}
     * @memberof CreateUserRequest
     */
    'id'?: string;
    /**
     * 氏
     * @type {string}
     * @memberof CreateUserRequest
     */
    'lastName'?: string;
    /**
     * 名
     * @type {string}
     * @memberof CreateUserRequest
     */
    'firstName'?: string;
    /**
     * 電話番号
     * @type {string}
     * @memberof CreateUserRequest
     */
    'tel'?: string;
    /**
     * メールアドレス
     * @type {string}
     * @memberof CreateUserRequest
     */
    'email': string;
    /**
     * ステータス。 - `valid` - 有効 - `invalid` - 無効 - `deleted` - 退会済み 
     * @type {string}
     * @memberof CreateUserRequest
     */
    'status'?: CreateUserRequestStatusEnum;
    /**
     * パスワード
     * @type {string}
     * @memberof CreateUserRequest
     */
    'password'?: string;
    /**
     * 作成日時
     * @type {string}
     * @memberof CreateUserRequest
     */
    'createdAt'?: string;
    /**
     * 更新日時
     * @type {string}
     * @memberof CreateUserRequest
     */
    'updatedAt'?: string;
}

export const CreateUserRequestStatusEnum = {
    VALID: 'valid',
    INVALID: 'invalid',
    DELETED: 'deleted'
} as const;

export type CreateUserRequestStatusEnum = typeof CreateUserRequestStatusEnum[keyof typeof CreateUserRequestStatusEnum];

/**
 * 
 * @export
 * @interface ListUser200Response
 */
export interface ListUser200Response {
    /**
     * 
     * @type {Array<ListUser200ResponseUsersInner>}
     * @memberof ListUser200Response
     */
    'users'?: Array<ListUser200ResponseUsersInner>;
}
/**
 * 
 * @export
 * @interface ListUser200ResponseUsersInner
 */
export interface ListUser200ResponseUsersInner {
    /**
     * ユーザーID
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'id'?: string;
    /**
     * 氏
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'lastName'?: string;
    /**
     * 名
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'firstName'?: string;
    /**
     * 電話番号
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'tel'?: string;
    /**
     * メールアドレス
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'email'?: string;
    /**
     * ステータス。 - `valid` - 有効 - `invalid` - 無効 - `deleted` - 退会済み 
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'status'?: ListUser200ResponseUsersInnerStatusEnum;
    /**
     * パスワード
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'password'?: string;
    /**
     * 作成日時
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'createdAt'?: string;
    /**
     * 更新日時
     * @type {string}
     * @memberof ListUser200ResponseUsersInner
     */
    'updatedAt'?: string;
}

export const ListUser200ResponseUsersInnerStatusEnum = {
    VALID: 'valid',
    INVALID: 'invalid',
    DELETED: 'deleted'
} as const;

export type ListUser200ResponseUsersInnerStatusEnum = typeof ListUser200ResponseUsersInnerStatusEnum[keyof typeof ListUser200ResponseUsersInnerStatusEnum];

あとは、生成された CreateUserRequestListUser200Response を実際のリクエスト送信やレスポンス受信時に利用するだけです。

まとめ

OpenAPI から仕様書の生成、コード(モデル定義)の自動生成を紹介しました。
少しでも良さが伝わっていれば幸いです。

機会があれば、次回はコード自動生成の具体例をさらに深掘りします。

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?