はじめに
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で生成したドキュメントが以下です。
如何でしょうか? とても見やすいドキュメントになっていますね。
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];
あとは、生成された CreateUserRequest
や ListUser200Response
を実際のリクエスト送信やレスポンス受信時に利用するだけです。
まとめ
OpenAPI から仕様書の生成、コード(モデル定義)の自動生成を紹介しました。
少しでも良さが伝わっていれば幸いです。
機会があれば、次回はコード自動生成の具体例をさらに深掘りします。