こんにちは。エンジニアの鈴木(@yamotuki)です。
今日はAPIドキュメントを書くことでフロントエンドとバックエンドの開発を疎結合にして平行して開発を進めている話を書こうと思います。
疎結合とは?
通常の開発フローだとバックエンドAPIを先に実装して、そのあとでフロントエンドの開発を進める必要があります。これはAPIからどのようなレスポンスが帰ってくるかわからないので、フロントエンドは先に実装することはできないと言う事情があります。では、APIを完全に実装しきってからではないとフロントエンドの開発がすすめられないのか、というとそうではないと考えています。
依存関係逆転の原則(DIP)の考え方を導入すると、フロントエンドが依存する対象を変えることができます。DIPを一言で言うと "詳細に依存するな、インターフェースに依存しろ" だと私は考えています。
今回のAPIとフロントエンドの結合部分においてはAPIの表向きのレスポンスフォーマット、すなわちインターフェースが重要です。レスポンスフォーマットとはレスポンスステータスやJSON形式などです。 フロントエンドもAPI実装のそのどちらもAPIのインターフェースだけに依存するようにすれば依存性逆転が実現できそうです。
APIインターフェースの定義の仕方はデファクトスタンダードとしてOpenAPIがあります。
OpenAPI (Swagger) とは
The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection.
要するに、APIのインターフェースを記述するための仕様です。 OpenAPIは version 3 からはOpenAPIと呼ばれており、version 2の時にはswaggerと呼ばれておりこちらのほうを聞いたことがある人も多いのではないかと思います。
最終的に書かれるインターフェース定義書はYAMLまたはJSONです。弊社ではSwagger-php
と言うライブラリを使ってアノテーションから自動生成させています(詳細は後述)。
例としてニュース一覧を返すAPI についての記述を以下に示します。レスポンスステータスは200で、内容は別途定義された schema の内容を返すことが定義されています。この記述は内部実装には依存しません。内部実装がこの仕様に依存するように実装していきます。
"/api/media/news": { "get": { "responses": { "200": { "description": "success", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/news_list_responder" } } } } } } },
PHP/Laravel でどのように書くか
PHPにOpenAPIを組み込むのはSwagger-php
と言うライブラリがあります。
弊社はバックエンドのフレームワークとしてLaravelを使っています。
Laravelに対しては L5-swagger と言うライブラリを使うと、上記のSwagger-php
に加えて人間がより読みやすい表現をブラウザ経由で見れるswagger-ui
を同時に導入できます。
GitHub - DarkaOnLine/L5-Swagger: Swagger integration to Laravel 5
This package is a wrapper of Swagger-php and swagger-ui adapted to work with Laravel 5.
Swagger-php の記法でかくと以下のようになります
/** * @OA\Get( * path="/api/media/news", * @OA\Response( * response="200", * description="success", * @OA\JsonContent(ref="#/components/schemas/news_list_responder") * ), * ) */
/** * @OA\Schema( * schema="news_summary", * required={"id", "title", "created_at"}, * @OA\Property(property="id", type="integer", example=1), * @OA\Property(property="title", type="string", example="タイトル"), * @OA\Property(property="created_at", type="string", example="2020-01-17T11:25:28+09:00"), * ) */ /** * @OA\Schema( * schema="news_list_responder", * @OA\Property( * property="news", * type="array", * @OA\Items( * ref="#/components/schemas/news_summary" * ), * ) * ) */
これがSwagger-php
によって解釈されると先に記述したJSON形式での仕様書になり、さらにswagger-uiの機能で以下のように人間が読みやすい形になります。
swagger-ui のブラウザで見たときのイメージ
これを参照してフロントエンドを実装することで、フロントエンドはAPIのインターフェースに依存することができました。 次はバックエンド実装が実際にAPIインターフェースと一致していることを確認する方法です。
API実装がAPIインターフェースに一致していることを自動テストする
この openapi-validator を使うことで、APIレスポンスが実際に定義に一致しているかを自動テストできます。 このライブラリはStar数もそんなに多くなく、ネット上に情報も少ないので使い方の参考実装も置いておきます。
テスト例。それぞれのAPIの仕様のテストはこれだけです。
public function testInvokeSpec() { $this->specTest('get', '/api/media/news', [], 200, NewsListGetAction::class); }
以下のコードで openapi-validator をラッピングしています。
protected function specTest(string $method, string $path, array $params, int $statusCode, string $actionClass): void { $method = strtolower($method); switch ($method) { case 'get': $response = $this->getJson($path, $params); break; case 'post': $response = $this->postJson($path, $params); break; case 'delete': $response = $this->deleteJson($path, $params); break; } $openApiValidator = OpenApiValidator::getValidator(); $specResult = $openApiValidator->validate($actionClass . '::__invoke', $statusCode, json_decode($response->getContent(), true)); echo $specResult; $this->assertEquals($specResult->hasErrors(), false); }
Validator インスタンスの取得コードです。毎回定義書を更新するたびにapi-docsを手動で再生成するのが面倒なので l5-swagger:generate を最初に叩いています。
use Illuminate\Support\Facades\Artisan; use Mmal\OpenapiValidator\Validator; class OpenApiValidator { protected static $openApiValidator; // ref: https://gitlab.com/mmalawski/openapi-validator public static function getValidator() { if (is_null(static::$openApiValidator)) { Artisan::call('l5-swagger:generate'); static::$openApiValidator = new Validator( json_decode(file_get_contents('./storage/api-docs/api-docs.json'), true)); } return static::$openApiValidator; } }
終わりに
この手法の良い点、悪い点をまとめておきます。
良い点
- フロントエンドとバックエンドを並行開発しやすい
- お互いの開発者は実装よりも仕様に集中できるので議論しやすい
- 先にAPI外部設計を議論して固めることにより実装がスムーズ
- APIレスポンスは仕様に沿っているか自動テストされているので保守が楽