企業ページリプレイス ~OpenAPIの活用~

こんにちは、21新卒エンジニアの柳です。

先日、PR TIMESの企業ページをSmartyというテンプレートエンジンからReactへリプレイスを行いました。その際にOpenAPIを社内のプロジェクトで初めて導入したので、OpenAPIのメリットや活用方法について書きたいと思います。

目次

プロジェクトの背景

OpenAPIの説明に行く前に、企業ページをReactへリプレイスするに至った背景について少しお話しします。

企業ページをReactへリプレイスを行うことになった背景は以下の2点です。

  • 現状使用されている JavaScript, jQuery では今後の機能追加に応えることが困難
  • PR TIMES全体のフロントエンドのレガシー改善プロジェクトの最初の一歩として

企業ページは企業の様々な情報を1つにまとめたページになっているためデータ量も多く、また非同期通信によるLoad Moreやアニメーションなどが JavaScript、jQuery、Vue.js で実装されており、現状のコードでは機能の追加や修正、改善も難しい状態でした。また、PR TIMESのサービス全体でこのようなフロントエンドのレガシー化が進んでいます。

今回のプロジェクトでは、このような状態を改善するための一歩として、既存のコードを一切しようせずにフルスクラッチでReactにリプレイスしました。

また、リプレイスにあたりバックエンドのAPI化が必要でしたが、諸事情でフロントエンドよりも遅れて開発することになりました。そこでAPIのインタフェース定義を起点に開発をするスキーマ駆動開発を行うために、OpenAPIを導入しました。

OpenAPIとは?

OpenAPIとは、RESTful APIの仕様を記述するためのフォーマットです。

このフォーマットに従ってAPIの仕様をyamlやjsonに記述することで、コードを自動生成できたり、モックサーバーを構築することができます。

以下がOpenAPIに沿って書かれた仕様書の例です。

swagger: '2.0'
info:
  title: PR TIMES API
  description: 企業ページ用API
  version: '1.0'
host: prtimes.jp
schemes:
  - https
produces:
  - application/json; charset=utf-8
consumes:
  - application/json; charset=utf-8
tags:
  - name: Company
    description: 企業情報
paths:
  '/companies/{company_id}':
    get:
      summary: Get Company Detail
      tags:
        - Company
      responses:
        '200':
          description: OK
          schema:
            $ref: '#/definitions/company_detail'
      operationId: get-company-by-id
      description: 企業情報API
    parameters:
      - type: integer
        name: company_id
        in: path
        required: true
        x-example: 112
definitions:
  company_detail:
    title: company_detail
    type: object
    properties:
      company_id:
        type: integer
        description: 企業ID
        example: 112
      company_name:
        type: string
        description: 企業名
        example: 株式会社PR TIMES
      description:
        type: string
        description: 企業説明文
        example: 株式会社PR TIMESの説明文

OpenAPIを導入するメリット

OpenAPIのフォーマットに沿ってAPI仕様書を記述することで以下のメリットがあります。

  • 仕様書を記述する人が変わってもフォーマットを統一して作成することができる
  • APIリファレンスドキュメントを自動生成することができるようになる
  • 簡単にモックサーバーを作成できるようになる
  • 仕様書からコードを自動生成することができるようになる
  • バックエンドのレスポンスが仕様書に沿っているかテストができるようになる

しかし、これらの恩恵を授かるには様々なツールが必要になります。これから実際に今回のプロジェクトで使用したツールをご紹介いたします。

Stoplight Studio

1つ目のツールは Stoplight Studio です。このツールはOpenAPIのフォーマットに沿った仕様書を記述することができるGUIエディタで、これを使用することで直感的にAPIの仕様を記述することができるようになります。

別のエディタで Swagger Editor というツールがありますが、こちらは yaml や json を直接記述する必要があるため、OpenAPIの記述に慣れていない方は Stoplight Studo がおすすめです。

Redoc

2つ目は Redoc です。こちらを使用することで仕様書をドキュメントとして閲覧することができるようになり、以下のようにとても見やすくなります。

デモ:https://redocly.github.io/redoc/#section/Introduction

Redoc以外にもドキュメントを表示するツールは様々あるので、お好みのものを選んでもらうと良いと思います。

Prism

3つ目のツールは Prism です。このツールを使用するとAPI仕様書からモックサーバーを起動できるようになります。今回のリプレイスプロジェクトではAPIサーバーの実装よりフロントエンドの実装が先行して開発を行なっていたため、非常に役立ちました。

以下のコマンドを実行することで初めにお見せした仕様書のモックサーバーを実際に起動させることができます。

$ docker run --rm -it -v $PWD:/tmp  -p 4010:4010 stoplight/prism:3 mock -h 0.0.0.0 /tmp/prtimes.yaml

実際にこのモックサーバーのレスポンスを見てみると仕様書通りのレスポンスが返却されることが確認出来ます。

$ curl -s -D /dev/stderr -X GET -H "Accept:application/json" http://localhost:4010/companies/112 | json_pp
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: *
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: *
Content-type: application/json; charset=utf-8
Content-Length: 87
Date: Thu, 21 Oct 2021 10:20:21 GMT
Connection: keep-alive

{
   "company_id" : 112,
   "company_name" : "PR TIMES",
   "description" : "PR TIMES description"
}

しかし、Prismを用いて開発していたときに不便と感じる場面も少しありました。

1つはURLのパスで指定しているcompany_idとレスポンスのデータのcompany_idが一致しないところです。これはPrismがexampleで指定したデータしか返さないのでしょうがない点です。一応、dynamicというオプションを使用すれば自動でダミーデータを返してくれますが、それでもパスのパラメータとデータが同期してくれることはないです。

もう1つは認証が必要なAPIでもexample通りのレスポンスしか返してくれない点です。これはサービスの認証方法によって実現が可能な場合と困難な場合がありますが、PR TIMESではcookieに付与されているsessionIDで認証を行っているため、うまくモックすることは出来ませんでした。API KeyやJWTを用いた方法であれば実現可能だと思います。

しかし、フロントエンドの大部分の機能は example で指定しているレスポンスで事足りました。そして、次に紹介するOpenAPI Generatorと組み合わせることで、エンドポイントを切り替えるだけで後から実装されたAPIサーバーと繋ぎ込みをすることができ、大幅に工数を短縮することができました。

OpenAPI Generator

4つ目のツールは OpenAPI Generator です。このツールを使用することでAPI仕様書からTypeScriptやPHPなどのコードを自動生成することができるようになります。今回のプロジェクトではフロントエンド用のTypeScriptのコードを自動生成して使用しました。

下記は自動生成されたコードの例になります。

export class CompanyApi extends runtime.BaseAPI {
	async getCompanyById(
		requestParameters: GetCompanyByIdRequest,
		initOverrides?: RequestInit,
	): Promise<Company> {
		const response = await this.getCompanyByIdRaw(
			requestParameters: GetCompanyByIdRequest,
			initOverrides?: RequestInit,
		);
		return await response.value();
	}
}

今回のプロジェクトではこの自動生成されたコードを以下のように React Query でラップし、APIからのデータを取得しました。

import { useQuery } from "react-query";
import { CompanyApi } from "~/api";

const companyApi = new CompanyApi();
const useCompany = (companyId: number) => {
	return useQuery(['company', companyId], async () => {
		return companyApi.getCompanyById({ companyId });
}

const Company = (): JSX.Element => {
	const { data: company, isLoading, isError } = useCompany(112);
	if (isLoading) return <div>...loading</div>;
	if (isError) return <div>Error</div>;
	return <h1>{ company.company_name }</h1>;
}

Dredd

最後のツールは Dredd です。このツールはAPIサーバーのレスポンスがAPI仕様書と一致しているかどうかをテストすることができるようになります。

実際に以下のAPI仕様書を使ってAPIをテストしてみたいと思います。

swagger: '2.0'
info:
  title: PR TIMES API
  description: 企業詳細用API
  version: '1.0'
host: prtimes.jp
schemes:
  - https
produces:
  - application/json; charset=utf-8
consumes:
  - application/json; charset=utf-8
tags:
  - name: User
    description: ユーザー情報
paths:
  /verify_user:
    get:
      tags:
        - User
      description: ユーザーがログインしているかどうか
      operationId: verify-user
      parameters: []
      responses:
        '200':
          description: OK
          schema:
            $ref: '#/definitions/verify_user'
          examples: {}
      summary: Verify User
definitions:
  verify_user:
    type: object
    title: verify_user
    description: ユーザーがログインしているかどうか
    properties:
      is_login:
        type: boolean
        description: ログインしているかどうか
        example: true
      user:
        type: object
        description: ユーザー
        x-nullable: true
        properties:
          type:
            type: string
            enum:
              - company
              - media
              - social
            example: social
            description: ログインユーザーのタイプ
        required:
          - type
    required:
      - is_login

テストで使用するAPIは localhost:3000 で動いており、以下のようなレスポンスを返します。

$ curl -s -D /dev/stderr -X GET -H "Accept:application/json" http://localhost:3000/verify_user | json_pp
HTTP/1.1 200 OK
X-Powered-By: Express
Vary: Origin, Accept-Encoding
Access-Control-Allow-Credentials: true
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
Content-Length: 61
ETag: W/"3d-7ciSK0uo9fQOLhJsyjmgWOLi5PE"
Date: Sat, 23 Oct 2021 15:13:12 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{
   "is_login" : true,
   "user" : {
      "type" : "company"
   }
}

このAPIは仕様書通りのレスポンスを返しているため、以下のようにテストをパスすることができます。

$ dredd ./prtimes.yaml http://localhost:3000
pass: GET (200) /verify_user duration: 64ms
complete: 1 passing, 0 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 66ms

次にAPIが仕様書とは異なるレスポンスを返すように変更してみます。

今回は、verify_usertypecompany media socialではなく dummy を返すように変更します。

すると、fail: body: At '/user/type' No enum match for: "dummy" と表示され、テストが失敗することが確認できました。

$ dredd ./prtimes.yaml http://localhost:3000
fail: GET (200) /verify_user duration: 82ms
info: Displaying failed tests...
fail: GET (200) /verify_user duration: 82ms
fail: body: At '/user/type' No enum match for: "dummy"

request: 
method: GET
uri: /verify_user
headers: 
    Content-Type: application/json; charset=utf-8
    Accept: application/json; charset=utf-8
    User-Agent: Dredd/14.0.0 (Darwin 20.6.0; x64)

body: 

expected: 
headers: 
    Content-Type: application/json; charset=utf-8

body: 
{
  "is_login": true,
  "user": {
    "type": "company"
  }
}
statusCode: 200
bodySchema: {"type":"object","title":"verify_user","properties":{"is_login":{"type":"boolean","examples":[true]},"user":{"type":["object","null"],"properties":{"type":{"type":"string","enum":["company","media","social"],"examples":["social"]}},"required":["type"]}},"required":["is_login"]}

actual: 
statusCode: 200
headers: 
    x-powered-by: Express
    vary: Origin, Accept-Encoding
    access-control-allow-credentials: true
    cache-control: no-cache
    pragma: no-cache
    expires: -1
    x-content-type-options: nosniff
    content-type: application/json; charset=utf-8
    content-length: 59
    etag: W/"3b-sT8VswXBCc6DVOhcLqxX4iiovbc"
    date: Sat, 23 Oct 2021 15:28:48 GMT
    connection: close

bodyEncoding: utf-8
body: 
{
  "is_login": true,
  "user": {
    "type": "dummy"
  }
}

complete: 0 passing, 1 failing, 0 errors, 0 skipped, 1 total
complete: Tests took 84ms
make: *** [test] Error 1

このように Dredd を利用することで、APIサーバーのレスポンスが仕様書通りであるかを確認することができるため、先程の OpenAPI Generator で生成されたコードを信頼して利用することができるようになります。

しかし、Dreddにも何点か不便なところがありました。1つはOpenAPI3には、まだ対応しておらずOpenAPI2(Swagger)で記述する必要があることです。もう1つは認証が必要なAPIをテストするのが困難点です。これは先ほど説明したPR TIMESの認証方法の問題になります。

以上のような不便と感じる点もありますが、OpenAPI2でもフォーマットを統一して記述することができますし、認証が必要なエンドポイントよりも必要のないエンドポイントの方がデータ量が多く目視での確認に時間がかかるため、そちらのテストができるだけでも良いと考えると導入したメリットはあると思いました。

まとめ

今回のプロジェクトでOpenAPIを利用してみて、様々なメリットを授かることができました。また、OpenAPIをプロジェクトに導入するのは PR TIMES内で初めての試みであったため、様々な知見を得ることができたと思います。

今まで PR TIMES ではバックエンドが開発されてからフロントエンドを開発するという方法が主流でしたが、今回のように OpenAPI を利用したスキーマ駆動開発を行うことでフロントエンド先行で開発できることがわかり、今後のPR TIMESの開発に良い影響があるのではないかと思います。

この記事を書いた人

株式会社PR TIMES 開発本部 フロントエンドエンジニア

目次
閉じる