フロント開発で使うAPIモック – API Blueprint
フロント開発を行う際に API 呼び出しをする機会があると思います。フロントを開発するのに集中したいのに API の環境を準備するなどの時間を使いたくはないですね。
そこで今回は API のモックサーバを API Blueprint を利用して準備する手順を書いていきます。
今回の手順はすべて Mac 上での手順となります。Winodws, Ubuntuでの動作確認はしておりませんのでご注意を。
プロジェクトの準備
Node.js のインストール
個人的には nodebrew での Node.js のバージョン管理がおすすめです。
インストール方法は上記のリンクかこちらのQiitaの記事を参考にしてください。
Yarn のインストール (npm でもいいよ)
Node.js をインストールすれば npm が一緒にインストールされるのでそれでも問題ないですが、個人的には Yarn の方が色々と早い気がするのでこちらを使います。
インストール方法は公式ドキュメントを参照してください。
プロジェクトの作成
今回は React を利用したプロジェクトを例にして、API モックサーバを利用していきます。
ではプロジェクトを作成していきましよう。
yarn global add create-react-app
create-react-app front-mock --typescript
cd front-mock
yarn start
これで自動的に React アプリのベースが作られ、自動的にブラウザが起動します。起動したら React ロゴが回転する画面が表示されます。
API Blueprint の準備
API Blueprint をインストールをして利用する準備を行いましよう。
yarn start
して起動中だと思うので、Ctrl + C
で止めてください。
API Blueprint を利用するには drakov を利用します。これのインストールにはなにかと時間がかかるので global にインストールすることをおすすめします。
yarn global add drakov
API Blueprint の定義を書く
API Blueprint を利用するには API の定義を書く必要があります。ファイルの形式は何でも良いのでしょうが md ファイルに記述していきます。
mkdir -p mock/api-blueprint
cd mock/api-blueprint/
vi api.md
api.md
の中はこちらを参照しつつ、記述してください。サンプルに1つの API を記述します。
FORMAT: 1A
# Front Mock API
# Group 本
## 本 [/api/books]
### 本リスト 取得 [GET]
+ Response 200 (application/json)
+ Attributes (array[Book], fixed-type)
# Data Structures
## Book (object)
+ id: `eb10484d-698f-43a3-b269-02ba810aa936` (string) - ID
+ name: `7つの習慣` (string) - 名前
+ locations: (array[string]) - 本の場所
+ `会社` (string)
+ `Aさんの家` (string)
+ `Bさんの家` (string)
API Blueprint を起動するコマンドを記述
これまでで API 定義は完了です。次に API Blueprint を起動させるためのコマンド(?)を記述します。
package.json
の scripts
に新しくコマンドを追加します。
"mock": "drakov -p 3002 -f 'mock/api-blueprint/**/*.md' --watch"
デフォルトで 3000 ポートで起動するので変更しておきます。
コマンドを追加したので、起動してみましょう!
$ yarn mock
yarn run v1.16.0
$ drakov -p 3002 -f 'mock/api-blueprint/**/*.md' --watch
[INFO] No configuration files found
[INFO] Loading configuration from CLI
DRAKOV STARTED
[LOG] Setup Route: GET /api/books 本リスト 取得
Drakov 1.0.4 Listening on port 3002
FILE SPY ACTIVE
色々と出力されますがポイントは2つ。
1つ目は [LOG] Setup Route: GET /api/books 本リスト 取得
。正しく API 定義がされていれば有効になっている API のメソッドとパスが表示されます。
2つ目は Drakov 1.0.4 Listening on port 3002
。このポートにアクセスすれば、今回定義した API を呼ぶことができます。
試しに http://localhost:3002/api/books
にアクセスしましょう。ブラウザでアクセスしても、curl でコールでもどちらでも OK です。
定義した body が表示されると思います。
定義ファイルの編集はリアルタイム反映
Data Structures
のセクションで「7つの習慣」と書いてあるところを「ティール組織」に変更してファイルを保存してください。
[CHANGE] mock/api-blueprint/api.md Restarting 1
DRAKOV STOPPED
DRAKOV STARTED
[LOG] Setup Route: GET /api/books 本リスト 取得
Drakov 1.0.4 Listening on port 3002
コンソール上では変更を検知して自動的に再起動しています。http://localhost:3002/api/books
にアクセスするとレスポンスの内容も変わっています。
モックの API を呼ぶ
モックの準備ができたところでアプリケーションから API を呼び出して見ましよう。API 呼び出しには axios (アクシオス) を利用します。
yarn add axios
App.tsx
で API 呼び出しをしましょう。
編集後の App.tsx
はこちら。
import React, { useEffect, useState } from 'react';
import axios, { AxiosResponse } from 'axios';
import './App.css';
class Book {
id: string;
name: string;
locations: string[];
constructor(id: string, name: string, locations: string[]) {
this.id = id;
this.name = name;
this.locations = locations;
}
}
const App: React.FC = () => {
const [books, setBooks] = useState<Book[]>([]);
useEffect(() => {
axios.get('http://localhost:3002/api/books')
.then((res: AxiosResponse<Book[]>) => setBooks(res.data))
.catch(console.error); // TODO エラー処理
}, []);
return (
<div className="app">
{books.map((book: Book) => (
<div key={book.id} className='book-item'>
<div className='book-id'>{book.id}</div>
<div className='book-name'>{book.name}</div>
<div className='book-locations'>{book.locations.join(', ')}</div>
</div>
))}
</div>
);
};
export default App;
編集後、yarn start
で起動するのですが、その際にモックを起動してほしいので、start
コマンドを修正します。
"start": "yarn mock & react-scripts start",
これで yarn start
を実行。
CORS な感じがいやな人はこちらも
このままだと http://localhost:3000
から http://localhost:8882
と違うホストへのリクエストとなります。開発時には問題にならないかもしれないですが、ステージングや本番環境で向き先を変える必要があります。環境変数でホストを指定する方法もありますが、もっとシンプルにできるようにしましょう!
まずは package.json
を修正。
"proxy": "http://localhost:3002"
この行を追加。
次に App.tsx
を修正。
useEffect(() => {
axios.get('/api/books')
.then((res: AxiosResponse<Book[]>) => setBooks(res.data))
.catch(console.error); // TODO エラー処理
}, []);
これで OK。自身のホストの /api/books
にリクエストを出します。あとは自動的にプロキシをしてくれます。
POST もやってみよう!
POST のリクエストの定義もやってみよう!
api-blueprint/api.md
に追加
### 本 追加 [POST]
+ Request (application/json)
+ Attributes (object)
+ name: (string) - ID
+ locations: (string) - 本の場所 (カンマ区切り)
+ Response 200 (application/json)
+ Attributes (object)
+ id: `2970ce4c-f331-4262-a2ec-3bc8e63ad6a8` - ID
App.tsx
も編集
import React, { useEffect, useState } from 'react';
import axios, { AxiosResponse } from 'axios';
import './App.css';
class Book {
id: string;
name: string;
locations: string[];
constructor(id: string, name: string, locations: string[]) {
this.id = id;
this.name = name;
this.locations = locations;
}
}
const App: React.FC = () => {
const [books, setBooks] = useState<Book[]>([]);
const [name, setName] = useState('');
const [locations, setLocations] = useState('');
useEffect(() => {
axios.get('/api/books')
.then((res: AxiosResponse<Book[]>) => setBooks(res.data))
.catch(console.error); // TODO エラー処理
}, []);
const onClickAdd = () => {
// TODO 入力チェックなど
axios.post('/api/books', { name, locations })
.then((res: AxiosResponse<{ id: string }>) => {
books.push(new Book(res.data.id, name, locations.split(',')));
setName('');
setLocations('');
setBooks(books);
})
.catch(console.error); // TODO エラー処理
};
return (
<div className="app">
<div>
{books.map((book: Book) => (
<div key={book.id} className='book-item'>
<div className='book-id'>{book.id}</div>
<div className='book-name'>{book.name}</div>
<div className='book-locations'>{book.locations.join(', ')}</div>
</div>
))}
</div>
<div className='book-form'>
<div>名前</div>
<div><input name='name' value={name} onChange={e => setName(e.target.value)}/></div>
<div>ロケーション (複数の場合はカンマ区切りで)</div>
<div><input name='locations' value={locations} onChange={e => setLocations(e.target.value)}/></div>
<div>
<button onClick={onClickAdd}>追加</button>
</div>
</div>
</div>
);
};
export default App;
POST の結果が常に同じ値を返すので一部で Warning が出るかもしれないですが、柔軟に対応してください。
PUT もやってみよう!
PUT のリクエストの定義もやってみよう!
api-blueprint/api.md
に追加
## 本1つ [/api/books/{id}]
+ Parameters
+ id: (string, required) - 本ID
### 本 更新 [PUT]
+ Request (application/json)
+ Attributes (object)
+ name: (string) - ID
+ locations: (string) - 本の場所 (カンマ区切り)
+ Response 200 (application/json)
App.tsx
も編集
import React, { useEffect, useState } from 'react';
import axios, { AxiosResponse } from 'axios';
import './App.css';
class Book {
id: string;
name: string;
locations: string[];
constructor(id: string, name: string, locations: string[]) {
this.id = id;
this.name = name;
this.locations = locations;
}
}
const App: React.FC = () => {
const [books, setBooks] = useState<Book[]>([]);
const [name, setName] = useState('');
const [locations, setLocations] = useState('');
const [editBookId, setEditBookId] = useState('');
const [editBookIndex, setEditBookIndex] = useState(-1);
useEffect(() => {
axios.get('/api/books')
.then((res: AxiosResponse<Book[]>) => setBooks(res.data))
.catch(console.error); // TODO エラー処理
}, []);
const onClickAdd = () => {
// TODO 入力チェックなど
axios.post('/api/books', { name, locations })
.then((res: AxiosResponse<{ id: string }>) => {
books.push(new Book(res.data.id, name, locations.split(',')));
setName('');
setLocations('');
setBooks(books);
})
.catch(console.error); // TODO エラー処理
};
const onDoubleClickBookRow = (book: Book, index: number) => {
setEditBookIndex(index);
setEditBookId(book.id);
setName(book.name);
setLocations(book.locations.join(','));
};
const onClickCancelEdit = () => {
setEditBookIndex(-1);
setEditBookId('');
setName('');
setLocations('');
};
const onClickSaveEdit = () => {
// TODO 入力チェックなど
axios.put(`/api/books/${editBookId}`, { name, locations })
.then(() => {
books[editBookIndex].name = name;
books[editBookIndex].locations = locations.split(',');
setEditBookIndex(-1);
setEditBookId('');
setName('');
setLocations('');
setBooks(books);
})
.catch(console.error); // TODO エラー処理
};
return (
<div className="app">
<div>
{books.map((book: Book, index: number) => (
<div key={book.id} className='book-item' onDoubleClick={() => onDoubleClickBookRow(book, index)}>
<div className='book-id'>
{editBookIndex === index ? (
<div>
<button onClick={onClickCancelEdit}>キャンセル</button>
<button onClick={onClickSaveEdit}>保存</button>
</div>
) : (
<span>{book.id}</span>
)}
</div>
<div className='book-name'>
{editBookIndex === index ? (
<input name='name' value={name} onChange={e => setName(e.target.value)}/>
) : (
<span>{book.name}</span>
)}
</div>
<div className='book-locations'>
{editBookIndex === index ? (
<input name='locations' value={locations} onChange={e => setLocations(e.target.value)}/>
) : (
<span>{book.locations.join(', ')}</span>
)}
</div>
</div>
))}
</div>
{editBookIndex === -1 && (
<div className='book-form'>
<div>名前</div>
<div><input name='name' value={name} onChange={e => setName(e.target.value)}/></div>
<div>ロケーション (複数の場合はカンマ区切りで)</div>
<div><input name='locations' value={locations} onChange={e => setLocations(e.target.value)}/></div>
<div>
<button onClick={onClickAdd}>追加</button>
</div>
</div>
)}
</div>
);
};
export default App;
ドキュメントを作成しよう!
API Blueprint で定義したファイルを HTML 形式で見ましょう!
まずは aglio をインストールします。
yarn global add aglio
スクリプトを追加します。
"mock-doc": "mkdir -p mock/api-blueprint/docs && aglio -i mock/api-blueprint/api.md -o mock/api-blueprint/docs/api.html && sed -i '' 's/<html>/<html lang=\"ja\">/' ./mock/api-blueprint/docs/api.html"
いくらか警告が出ますが、気になる方は修正してください。
open ./mock/api-blueprint/docs/api.html
これでブラウザで API の定義を確認できます。
最後に
あとは api-blueprint/api.md
の定義を増やしていくことで色々な API を再現できます。
サンプルのレスポンスで複数返す方法とかがなにかとわかっていないですが、がんばりましょう!
なにかあれば質問箱から質問をください。