restful-reactで始めるOpenAPI

10 min読了の目安(約9200字TECH技術記事

はじめに

OpenAPIを使って開発をしている方のほとんどは、何かしらのコードジェネレーターを使っているかと思います。
その中でも今回は、自分が最近productionでも使用しているrestful-reactを紹介したいと思います。

restful-reactとは

OpenAPIのymlを読み込んで、いい感じの型情報やリクエスト用のフックを生成してくれるものです。

イケてるうえに簡単なので、コードをみた方が早いと思います。
さっそく使ってみましょう!

インストール

yarn add restful-react

型を生成してみる

Get localhost:8080/users
このような、ユーザー一覧を取得するエンドポイントを想定してみましょう。

まずopenapiのymlを定義します。

schema.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 に生成用のコマンドを追加します。

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.

生成されたファイルはこのようになると思います。

src/api/generate.tsx
/* 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);

これだけでも十分開発が捗りそうです!

リクエストしてみる

先ほど生成した型情報を使って、実際にリクエストしてみます。

src/App.tsx
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を指定します。

src/index.tsx
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パラメータが含まれるエンドポイントにリクエストをしてみたいと思います。

まずスキーマを修正します。

schema.yml
...
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

先ほど生成したコードに以下が追加されると思います。

src/api/generate.tsx
...

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 });
...

では、リクエストしてみましょう。

src/App.tsx
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をリクエストすることを想定してみましょう。

schema.yml
...
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
src/App.tsx
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: '木村', 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を使おうというお話みたいです。たぶんそういうことなんだよね?
参考↓

生成されるコードも見てみまよう。

schema.yml
  schemas:
    user:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
        role:
          type: string
          enum: [read, write] # unionになる
      required:
        - id
        - name
src/api/generate.tsx
export interface User {
  id: number;
  name: string;
  email?: string;
  role?: "read" | "write"; // enumではない
}

さいごに

いかがでしょうか。まだ説明してないオプションも何個かあるのですが、ここまでの情報があれば一通りの開発はできるのではないでしょうか?
最初にも書きましたが、自分は実際にproductionで使っているのでバックエンドとやりとりをする部分に関してはかなり高速に開発できています。

みなさんもぜひ使ってみてください!

この記事伸びるようならバックエンド編も書こうと思います。