restful-reactで始めるOpenAPI
はじめに
OpenAPIを使って開発をしている方のほとんどは、何かしらのコードジェネレーターを使っているかと思います。
その中でも今回は、自分が最近productionでも使用しているrestful-reactを紹介したいと思います。
restful-reactとは
OpenAPIのymlを読み込んで、いい感じの型情報やリクエスト用のフックを生成してくれるものです。
イケてるうえに簡単なので、コードをみた方が早いと思います。
さっそく使ってみましょう!
インストール
yarn add restful-react
型を生成してみる
Get localhost:8080/users
このような、ユーザー一覧を取得するエンドポイントを想定してみましょう。
まずopenapiのymlを定義します。
openapi: 3.0.0
info:
description: "restful-react example."
title: "test endpoints"
version: "1.0.0"
servers:
- url: http://localhost:8080
paths:
/users:
get:
description: get users
operationId: get users
responses:
200:
$ref: "#/components/responses/users"
components:
schemas:
user:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
required:
- id
- name
- email
responses:
users:
description: users
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/user"
package.json
に生成用のコマンドを追加します。
{
"name": "restful-react-example",
"version": "0.1.0",
"private": true,
"dependencies": {
...
"restful-react": "^15.1.3",
...
},
...
"scripts": {
...
"generate": "restful-react import --file ./schema.yml --output ./src/api/generate.tsx"
}
}
では、コードを生成してみましょう。
yarn run generate
成功すれば、以下のようなログが出力されます。
yarn run v1.22.4
$ restful-react import --file ./schema.yml --output ./src/api/generate.tsx
🎉 Your OpenAPI spec has been converted into ready to use restful-react components!
✨ Done in 0.97s.
生成されたファイルはこのようになると思います。
/* Generated by restful-react */
import React from "react";
import { Get, GetProps, useGet, UseGetProps } from "restful-react";
export interface User {
id: number;
name: string;
email?: string;
}
/**
* users
*/
export type UsersResponse = User[];
export type GetUsersProps = Omit<GetProps<UsersResponse, unknown, void, void>, "path">;
/**
* get users
*/
export const GetUsers = (props: GetUsersProps) => (
<Get<UsersResponse, unknown, void, void>
path={`/users`}
{...props}
/>
);
export type UseGetUsersProps = Omit<UseGetProps<UsersResponse, unknown, void, void>, "path">;
/**
* get users
*/
export const useGetUsers = (props: UseGetUsersProps) => useGet<UsersResponse, unknown, void, void>(`/users`, props);
重要な部分だけ見てみましょう。
まず、ymlの components/schemas/user
に対応しているのが、
export interface User {
id: number;
name: string;
email?: string;
}
次に、ymlの components/responses/users
に対応してるのが、
export type UsersResponse = User[];
最後に、 paths/users
で定義したエンドポイントへのリクエスト用のカスタムフックが以下になります。
export const useGetUsers = (props: UseGetUsersProps) => useGet<UsersResponse, unknown, void, void>(`/users`, props);
これだけでも十分開発が捗りそうです!
リクエストしてみる
先ほど生成した型情報を使って、実際にリクエストしてみます。
import React from 'react'
import { useGetUsers } from './api/generate'
export const App = () => {
const { data, loading, error } = useGetUsers({}) // これでリクエストされる
if (loading) return <div>loading</div>
if (error) return <div>{error}</div>
if (!data) return <div>ユーザーが見つかりません</div>
return (
<div>
{data.map(user => (
<div>
<div>{user.id}</div>
<div>{user.name}</div>
<div>{user.email || 'none'}</div>
</div>
))}
</div>
)
}
先ほど生成した useGetUsers
は、マウント時にリクエストをします。
リクエストに成功すると data
にレスポンスが格納されます。
dataの型は先ほど生成した型が使われていて UsersResponse | null
型になっています。
リクエスト先はrestful-reactの RestfulProvider
でURLを指定します。
import React from 'react'
import ReactDOM from 'react-dom'
import { RestfulProvider } from 'restful-react'
import { App } from './App'
ReactDOM.render(
<React.StrictMode>
{/* URLを指定 */}
<RestfulProvider base="http://localhost:8080">
<App />
</RestfulProvider>
</React.StrictMode>,
document.getElementById('root'),
)
とっても簡単ですね。しっかり型もついてるので安心です。
では実行してみましょう。
yarn run start
ちゃんと localhost:8080/users
にGetのリクエストがされています!
パラメーターを使う
Get localhost:8080/users/{id}
このような、URLパラメータが含まれるエンドポイントにリクエストをしてみたいと思います。
まずスキーマを修正します。
...
paths:
# 追加
/users/{id}:
get:
description: get user
operationId: get user
parameters:
- $ref: "#/components/parameters/id"
responses:
200:
description: ok
content:
application/json:
schema:
$ref: "#/components/schemas/user"
...
components:
# 追加
parameters:
id:
name: id
in: path
description: id
required: true
schema:
type: integer
minimum: 0
...
スキーマを修正したら、コードを生成しなおしましょう。
yarn run generate
先ほど生成したコードに以下が追加されると思います。
...
export interface GetUserPathParams {
/**
* id
*/
id: number
}
export type GetUserProps = Omit<GetProps<User, unknown, void, GetUserPathParams>, "path"> & GetUserPathParams;
/**
* get user
*/
export const GetUser = ({id, ...props}: GetUserProps) => (
<Get<User, unknown, void, GetUserPathParams>
path={`/users/${id}`}
{...props}
/>
);
export type UseGetUserProps = Omit<UseGetProps<User, unknown, void, GetUserPathParams>, "path"> & GetUserPathParams;
/**
* get user
*/
export const useGetUser = ({id, ...props}: UseGetUserProps) => useGet<User, unknown, void, GetUserPathParams>((paramsInPath: GetUserPathParams) => `/users/${paramsInPath.id}`, { pathParams: { id }, ...props });
...
では、リクエストしてみましょう。
import React from 'react'
import { useGetUser } from './api/generate'
export const App = () => {
const { data, loading, error } = useGetUser({
id: 2, // ここでidを入れる
})
if (loading) return <div>loading</div>
if (error) return <div>{error}</div>
if (!data) return <div>ユーザーが見つかりません</div>
return (
<div>
<div>{data.id}</div>
<div>{data.name}</div>
<div>{data.email || 'none'}</div>
</div>
)
}
URLパラメーターやクエリパラメータはフックのオプションとして指定します。
では確認してみましょう。
localhost:8080/users/2
にリクエストがされていますね!
bodyを使う
Post localhost:8080/users
ユーザーを作成するエンドポイントにjsonをリクエストすることを想定してみましょう。
...
paths:
...
/users:
...
post:
description: create user
operationId: create user
responses:
200:
$ref: "#/components/schemas/user"
requestBody:
$ref: "#/components/requestBodies/createUser"
components:
...
requestBodies:
createUser:
description: create user
content:
application/json:
schema:
type: object
properties:
name:
type: string
email:
type: string
import React from 'react'
import { useCreateUser } from './api/generate'
export const App = () => {
const { mutate, loading, error } = useCreateUser({
onMutate: (_, res) => {
// mutateが呼ばれた後に呼ばれる
window.location.href = `http://localhost:3000/users/${res.id}`
},
})
if (loading) return <div>loading</div>
if (error) return <div>{error}</div>
return (
<div>
<button
onClick={() => {
// mutateに `components/requestBodies/createUser` の情報を渡す
mutate({ name: 'Alice', email: 'user-3@example.com' })
}}
>
create
</button>
</div>
)
}
mutationの場合に生成されるフックには、 mutate
という関数があります。
この関数を呼ぶことでリクエストされます。逆に言うと、呼ぶまでリクエストされませんので注意してください。
また、フックのオプション onMutate
に登録した関数は、 mutate
が呼ばれた後に呼ばれるので、リソースを作成したあとに、その詳細画面に遷移したい時などに便利です。
外部サービスにも使いたい
自社のバックエンド以外にリクエストする場合にもrestful-reactを使うことができます。
ただ、その場合はコードを生成することはできないので、自分でURLを指定してリクエストする形になります。
import React from 'react'
import { useGet, useMutate } from 'restful-react'
export const App = () => {
const { data, loading, error } = useGet({
path: 'https://example.com/foo/1',
})
const { mutate, loading, error } = useMutate({
path: 'https://example.com/foo/1',
verb: 'DELETE',
})
気になるところ
enumがunionで生成される
なぜかは分かりませんがちょっと不便ですね、、、って書こうと思ったんですが、どうやらenumよりもunionを使おうというお話みたいです。たぶんそういうことなんだよね?
参考↓
生成されるコードも見てみまよう。
schemas:
user:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
role:
type: string
enum: [read, write] # unionになる
required:
- id
- name
export interface User {
id: number;
name: string;
email?: string;
role?: "read" | "write"; // enumではない
}
さいごに
いかがでしょうか。まだ説明してないオプションも何個かあるのですが、ここまでの情報があれば一通りの開発はできるのではないでしょうか?
最初にも書きましたが、自分は実際にproductionで使っているのでバックエンドとやりとりをする部分に関してはかなり高速に開発できています。
みなさんもぜひ使ってみてください!
Discussion