こんにちは。
株式会社アドグローブ ソリューション事業部の内田です。
私は社内のもくもく会という勉強会に参加していて、そこでフロント開発の勉強をしています。
本記事では、もくもく会でモックの実装をした際に使用した「OpenAPI Generator」と「Json Server」についてご紹介します。
少しでもお役に立てれば幸いです。
今回の記事を読まれるにあたり、弊社ブログで金さんが書かれた記事をご紹介させていただきます!
私はこの記事を参考にOpenAPI Generatorの実装ができました。とても理解しやすい内容となっていますので、ぜひご覧になって下さい。
blog.adglobe.co.jp
もくもく会とは
毎週火曜日 19:15~20:15に開催される勉強会のことです。
現在は社員紹介システムのチーム開発を行っており、私はフロント側の開発をしています。
フロント側の使用言語は、Next.js14(app router)/React/TypeScriptがメインです。
初めて触る言語なので、毎週もくもく勉強しながら開発を進めています。
ベテランエンジニアの方も参加しており、分からないことがあれば質問できるのでいい環境だなと思います。
Json Serverを使用してAPIを作成してみる
当初はmsw(Mock Service Worker)を使用してモックを作成する予定でした。
しかし、Next.js14だとうまく動かなく、開発が止まりそうだったので、Json Serverに代替えしました。
(下記isseuを参考にしたのですが、SSRでmswが動かない問題が発生しました。)
Next.js14 app router + msw:https://github.com/mswjs/examples/pull/101/files
1. Json Server インストール
npm install json-server
2. レスポンスデータを用意
Root経路にdb.jsonファイルを作成後、下記コードを作成します。
{
"employees": [
{
"employeeNumber": 1,
"lastName": "内田",
"firstName": "太郎",
"lastNameKana": "ウチダ",
"firstNameKana": "タロウ",
"departmentId": 1,
"divisionId": 1,
"locationId": 1,
"dateOfEntry": "2024-04-01",
"hobby": "ゲーム",
"workHistory": "バックエンド",
"challenge": "Lravel",
"imageUrls": ["https://placehold.jp/300x200.png", "https://placehold.jp/600x400.png"]
}
]
}
3. スクリプト追加
package.jsonに以下のスクリプトを追加してください。
"scripts": {
...
"json-server": "json-server --watch db.json -p 8080"
},
4. スクリプト実行
npm run json-server
ターミナルでJson Serverが起動したことを確認できました。
PostmanでAPIの疎通確認もできました。
OpenAPI Generatorで作成されたAPIの疎通確認をしてみる
今回紹介するのは、自動生成されたコードを使用して、Json Serverで作成したAPIから値を取得するところまでいきたいと思います。
1. 事前準備
金さんの記事でも紹介されていましたが、
NPMでインストールする場合、OpenAPI GeneratorはJVMの基に動作するため、事前にJDKのインストールが必要です。
JDKインストール(Wnidows編)
2. openapi-generator-cli インストール
npm i @openapitools/openapi-generator-cli
3. Generator Configファイル作成
Root経路にopenapitools.jsonファイルを作成後、下記のコードを作成します。
今回はTypeScriptとFetchを使うため、typescript-fetch変換方法を採用しました。
{
"$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "6.2.1",
"generators": {
"v3.0": {
"generatorName": "typescript-fetch", // 変換方法
"output": "./src/lib/open-api", // スクリプト実行後のファイル生成先
"glob": "./docs/schema.yml" // OpenAPI仕様書の格納先
}
}
}
}
4. OpenAPI仕様書を作成する
schema.ymlファイルを作成します。
こちらが、もくもく会で使用しているOpenAPI仕様書となります。
openapi: 3.0.0
info:
title: 社員紹介ポータル API
version: "1.0"
servers:
- url: http://localhost:3000/api/v1
paths:
"/employees":
get:
tags:
# NOTE: classが生成される際に'<タグ名>API'というclass名になる
- employee
description: 社員情報一覧取得API
operationId: getEmployees
parameters:
- name: name
in: query
description: 名前
schema:
type: string
example: "田中太郎"
- name: divisionIds
in: query
description: 事業部区分IDの配列
schema:
type: array
items:
type: number
example: 1
- name: locationIds
in: query
description: 拠点IDの配列
schema:
type: array
items:
type: number
example: 1
responses:
"200":
description: "OK"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Employee"
"400":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Bad Request
"401":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Forbidden
"404":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Not Found
"500":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Internal Server Error
"503":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Service Unavailable
"/employees/{employeeNumber}":
get:
tags:
# NOTE: classが生成される際に'<タグ名>API'というclass名になる
- employee
description: 社員情報取得API
operationId: getEmployee
parameters:
- name: employeeNumber
in: path
description: 社員番号
required: true
schema:
type: number
responses:
"200":
description: "OK"
content:
application/json:
schema:
$ref: "#/components/schemas/Employee"
"400":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Bad Request
"401":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Forbidden
"404":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Not Found
"500":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Internal Server Error
"503":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Service Unavailable
"/departments":
get:
tags:
# NOTE: classが生成される際に'<タグ名>API'というclass名になる
- department
description: 事業部一覧取得API
operationId: getDepartments
responses:
"200":
description: "OK"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Department"
"400":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Bad Request
"401":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Forbidden
"404":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Not Found
"500":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Internal Server Error
"503":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Service Unavailable
"/locations":
get:
tags:
# NOTE: classが生成される際に'<タグ名>API'というclass名になる
- location
description: 拠点一覧取得API
operationId: getLocation
responses:
"200":
description: "OK"
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/Location"
"400":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Bad Request
"401":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Unauthorized
"403":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Forbidden
"404":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Not Found
"500":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Internal Server Error
"503":
content:
application/json:
schema:
$ref: "#/components/schemas/ErrorResponse"
description: Service Unavailable
components:
schemas:
Employee:
description: 社員情報
type: object
properties:
employeeNumber:
description: 社員番号
type: number
example: 1
lastName:
description: 姓
type: string
example: "田中"
firstName:
description: 名
type: string
example: "太郎"
lastNameKana:
description: 姓カナ
type: string
example: "タナカ"
firstNameKana:
description: 名カナ
type: string
example: "タロウ"
departmentId:
description: 事業部ID
type: number
example: 1
divisionId:
description: 事業部区分ID
type: number
example: 1
locationId:
description: 拠点ID
type: number
example: 1
dateOfEntry:
description: 入社日
type: string
format: date
example: "2023-02-01"
hobby:
description: 趣味
type: string
example: "ロッククライミング"
workHistory:
description: これまでの業務経歴
type: string
example: "Webシステム開発"
challenge:
description: チャレンジしたいこと
type: string
example: "Next.jsでアプリケーション開発"
imageUrls:
description: 画像のURL
type: array
items:
type: string
example: "https://xxxx.xxx/sample"
required:
- id
- employeeNumber
- lastName
- firstName
- lastNameKana
- firstNameKana
- departmentId
- locationId
- dateOfEntry
- hobby
- workHistory
- challenge
- imageUrls
Department:
description: 事業部マスタ
type: object
properties:
id:
description: 事業部ID
type: number
example: 1
name:
description: 事業部名
type: string
example: "ソリューション事業本部"
colorCode:
description: 事業部カラーコード
type: string
example: "#ff7f50"
divisions:
description: 事業部が持つ区分(ソリューション第〇事業部)
type: array
items:
$ref: "#/components/schemas/Division"
required:
- id
- name
- colorCode
- divisions
Division:
description: 事業部区分
type: object
properties:
id:
description: 事業部区分ID
type: number
example: 1
name:
description: 事業部区分名
type: string
example: "ソリューション第一事業部"
code:
description: 略称
type: string
example: "SL1"
required:
- id
- name
- code
Location:
description: 拠点マスタ
type: object
properties:
id:
description: 拠点ID
type: number
example: 1
name:
description: 拠点名
type: string
example: "東京"
required:
- id
- name
ErrorResponse:
description: Error Schema
type: object
properties:
errorCode:
type: string
description: "エラーコード"
example: "E-0001"
message:
type: string
description: "エラーメッセージ"
example: "Some Error"
自分で作成してみたい方はこちらのSwagger Editorを参照にすると分かりやすいかと思います。
5. スクリプト追加
package.jsonに以下のスクリプトを追加してください。
"scripts": {
...
"openapi-generate": "openapi-generator-cli generate",
},
6. スクリプト実行
手順3で設定したoutput値に変換されたファイルが保存されます。
npm run openapi-generate
7. スクリプト実行後のファイル確認
・apisフォルダ
OpenAPI仕様書を元に、APIをコールするクラスが格納されています。
・modelsフォルダ
OpenAPI仕様書を元に、レスポンス項目と型(インターフェース)が格納されています。
※基本的に自動生成されたファイルを直接更新することはありません。
8. 社員一覧ページを作成する
以下のサンプルコードを作成します。
※OpenAPI仕様書の定義によってimportする値が変わってくるので、確認が必要です。
src/app/example/employee/pages.tsx
import { EmployeeApi, Configuration } from "@/lib/open-api";
const EmployeePage = async () => {
const configuration = new Configuration({
basePath: process.env.NEXT_PUBLIC_API_BASE_URL ?? "",
});
const api = new EmployeeApi(configuration);
const employees = await api.getEmployees();
return (
div
h1テストページh1
{employees && (
div
{employees.map((employee) => (
ul key={employee.employeeNumber}
li{employee.lastName + employee.firstName}li
ul
))}
div
)}
div
);
};
export default EmployeePage;
サンプルコードの3行目でインポートしたファイルは、自動生成されたコードを集約しているので単一のインポートでアクセスできるようになっています。
このことをバレル(Barrel)と呼ぶらしい・・・
src/lib/open-api/index.tsx
export * from './runtime';
export * from './apis';
export * from './models';
Root経路の.env.localファイルに環境変数を作成します。
NEXT_PUBLIC_API_BASE_URL=http:
9. APIの疎通を確認する
Json Serverが起動している状態で画面を表示します。
簡素な画面ですが、Json ServerのAPIから名前を取得することができました。
まとめ
今回はもくもく会で実装した技術についてご紹介しました。
OpenAPI仕様書から自動的にコードを生成してくれる機能はとても魅力的だと感じました。
案件でもOpenAPIに触れる機会があるので、もっと勉強していきたいと思います。
今後もブログを書く機会があれば、もくもく会で学んだ技術について共有したいと思っています。
最後までお読みいただきありがとうございました!