mybook

ドキュメンテーション — 使いたくなるAPIドキュメント

ドキュメントなきAPIの悲劇

「使い方がわからないので、採用を見送ります」

新しいパートナー候補からのメール。テックブリッジのAPIは機能的には優れていたが、ドキュメントが貧弱だった。

「README.mdに箇条書きで書いてあるだけです」

サクラはPCの前で頭を抱えた。Stripeのドキュメントを見たとき、あの完成度に感動した記憶がある。コード例が動く、Try it出来る、エラーが何を意味するかが書いてある。

「ドキュメントもAPIの一部だ」

OpenAPI仕様

OpenAPI Specification(旧Swagger)は、APIを機械可読な形式で記述する業界標準だ。

# openapi.yaml
openapi: "3.1.0"
info:
  title: TechBridge API
  version: "2.0"
  description: |
    テックブリッジのパブリックAPI。
    
    ## 認証
    すべてのAPIリクエストには `X-API-Key` ヘッダーが必要です。
    
    ## レート制限
    プランによって異なります。詳細は[レート制限ページ](/docs/rate-limits)を参照してください。
  contact:
    email: api-support@techbridge.jp
    url: https://docs.techbridge.jp
  license:
    name: MIT
 
servers:
  - url: https://api.techbridge.jp/v2
    description: 本番環境
  - url: https://api-staging.techbridge.jp/v2
    description: ステージング環境
 
security:
  - ApiKeyAuth: []
 
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
 
  schemas:
    User:
      type: object
      required:
        - id
        - name
        - email
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          minLength: 2
          maxLength: 50
          example: "田中太郎"
        email:
          type: string
          format: email
          example: "tanaka@example.com"
        created_at:
          type: string
          format: date-time
          example: "2024-01-15T09:00:00Z"
 
    Error:
      type: object
      required:
        - type
        - status
        - detail
      properties:
        type:
          type: string
          example: "https://docs.techbridge.jp/errors/not_found"
        title:
          type: string
          example: "Not Found"
        status:
          type: integer
          example: 404
        detail:
          type: string
          example: "User (id: 999) が見つかりません"
        request_id:
          type: string
          example: "req_abc123"
 
paths:
  /users:
    get:
      summary: ユーザー一覧
      description: |
        ユーザーの一覧を返します。
        
        ### フィルタリング
        `status` パラメーターでステータスをフィルタリングできます。
        
        ### ソート
        `sort_by` と `order` パラメーターでソートできます。
      operationId: listUsers
      tags:
        - Users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
          description: ページ番号
        - name: per_page
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: 1ページあたりの件数
        - name: status
          in: query
          schema:
            type: string
            enum: [active, inactive, suspended]
          description: ステータスフィルター
      responses:
        "200":
          description: 成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/User"
                  meta:
                    type: object
                    properties:
                      pagination:
                        $ref: "#/components/schemas/Pagination"
              example:
                data:
                  - id: 1
                    name: "田中太郎"
                    email: "tanaka@example.com"
                    created_at: "2024-01-15T09:00:00Z"
                meta:
                  pagination:
                    current_page: 1
                    per_page: 20
                    total_pages: 5
                    total_count: 100
        "401":
          description: 認証エラー
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/Error"

rswagでドキュメントを自動生成

rswagはRSpecテストからOpenAPIドキュメントを生成するgemだ。テストとドキュメントが常に同期する。

# Gemfile
group :development, :test do
  gem 'rswag-api'
  gem 'rswag-ui'
  gem 'rswag-specs'
end
 
# インストール
rails generate rswag:install
 
# config/routes.rb
mount Rswag::Ui::Engine => "/api-docs"
mount Rswag::Api::Engine => "/api-docs"

rswagテストの書き方

# spec/requests/api/v2/users_spec.rb
require "swagger_helper"
 
RSpec.describe "Users API", type: :request do
  path "/api/v2/users" do
    get "ユーザー一覧を取得する" do
      tags "Users"
      produces "application/json"
      security [ApiKeyAuth: []]
 
      parameter name: :page, in: :query, type: :integer, default: 1,
                description: "ページ番号"
      parameter name: :per_page, in: :query, type: :integer, default: 20,
                description: "1ページあたりの件数(最大100)"
      parameter name: :status, in: :query, type: :string,
                enum: %w[active inactive], description: "ステータスフィルター"
 
      response "200", "成功" do
        schema type: :object,
               properties: {
                 data: {
                   type: :array,
                   items: { "$ref" => "#/components/schemas/User" }
                 },
                 meta: { "$ref" => "#/components/schemas/PaginationMeta" }
               }
 
        let(:"X-API-Key") { create(:api_key).key }
        let(:page) { 1 }
        let(:per_page) { 20 }
 
        before { create_list(:user, 3) }
 
        run_test! do |response|
          data = JSON.parse(response.body)
          expect(data["data"].length).to eq(3)
          expect(data["meta"]["pagination"]["total_count"]).to eq(3)
        end
      end
 
      response "401", "認証エラー" do
        schema "$ref" => "#/components/schemas/Error"
 
        let(:"X-API-Key") { "invalid_key" }
 
        run_test! do |response|
          body = JSON.parse(response.body)
          expect(body["status"]).to eq(401)
        end
      end
    end
 
    post "ユーザーを作成する" do
      tags "Users"
      consumes "application/json"
      produces "application/json"
      security [ApiKeyAuth: []]
 
      parameter name: :user, in: :body, schema: {
        type: :object,
        required: %w[name email],
        properties: {
          name: { type: :string, example: "田中太郎" },
          email: { type: :string, format: :email, example: "tanaka@example.com" }
        }
      }
 
      response "201", "作成成功" do
        schema "$ref" => "#/components/schemas/UserResponse"
 
        let(:"X-API-Key") { create(:api_key).key }
        let(:user) { { name: "田中太郎", email: "tanaka@example.com" } }
 
        run_test!
      end
 
      response "422", "バリデーションエラー" do
        schema "$ref" => "#/components/schemas/ValidationError"
 
        let(:"X-API-Key") { create(:api_key).key }
        let(:user) { { name: "", email: "invalid" } }
 
        run_test! do |response|
          body = JSON.parse(response.body)
          expect(body["errors"]).not_to be_empty
        end
      end
    end
  end
end

ドキュメント生成コマンド:

rails rswag:specs:swaggerize

生成された swagger/v2/swagger.yaml から、/api-docs でSwagger UIが表示される。

swagger_helper.rb

# spec/swagger_helper.rb
require "rails_helper"
 
RSpec.configure do |config|
  config.swagger_root = Rails.root.join("swagger").to_s
 
  config.swagger_docs = {
    "v2/swagger.yaml" => {
      openapi: "3.1.0",
      info: {
        title: "TechBridge API",
        version: "v2",
        description: "テックブリッジ パブリックAPI"
      },
      paths: {},
      servers: [
        { url: "https://api.techbridge.jp", description: "本番" }
      ],
      components: {
        securitySchemes: {
          ApiKeyAuth: {
            type: :apiKey,
            in: :header,
            name: "X-API-Key"
          }
        },
        schemas: {
          User: {
            type: :object,
            properties: {
              id: { type: :integer },
              name: { type: :string },
              email: { type: :string, format: :email },
              created_at: { type: :string, format: "date-time" }
            }
          },
          Error: {
            type: :object,
            properties: {
              type: { type: :string },
              status: { type: :integer },
              detail: { type: :string },
              request_id: { type: :string }
            }
          },
          ValidationError: {
            type: :object,
            properties: {
              type: { type: :string },
              status: { type: :integer },
              errors: {
                type: :array,
                items: {
                  type: :object,
                  properties: {
                    field: { type: :string },
                    code: { type: :string },
                    message: { type: :string }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
 
  config.swagger_format = :yaml
end

INFO

rswagの最大のメリットは「テストとドキュメントの同期」です。テストが通ればドキュメントも正確。ドキュメントだけ先に書いて実装と乖離する問題がなくなります。

Developer Portalの構築

Loading diagram...
# app/controllers/docs_controller.rb
class DocsController < ApplicationController
  skip_before_action :authenticate!
 
  def index
    render "docs/index"
  end
 
  def getting_started
    render "docs/getting_started"
  end
 
  def changelog
    @changes = ApiChangelog.order(released_at: :desc).page(params[:page])
    render "docs/changelog"
  end
end
 
# config/routes.rb
get "/docs", to: "docs#index"
get "/docs/getting-started", to: "docs#getting_started"
get "/docs/changelog", to: "docs#changelog"

コードサンプルの多言語対応

ドキュメントには複数言語のサンプルを載せる。

# users一覧取得のサンプル
 
# Ruby
require 'net/http'
require 'json'
 
uri = URI('https://api.techbridge.jp/v2/users')
uri.query = URI.encode_www_form(page: 1, per_page: 20)
 
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
 
request = Net::HTTP::Get.new(uri)
request['X-API-Key'] = 'your_api_key'
 
response = http.request(request)
users = JSON.parse(response.body)['data']
# Python
import requests
 
response = requests.get(
    'https://api.techbridge.jp/v2/users',
    headers={'X-API-Key': 'your_api_key'},
    params={'page': 1, 'per_page': 20}
)
 
users = response.json()['data']
// JavaScript (fetch)
const response = await fetch('https://api.techbridge.jp/v2/users?page=1&per_page=20', {
  headers: {
    'X-API-Key': 'your_api_key'
  }
});
 
const { data: users } = await response.json();

変更履歴(Changelog)の管理

# db/migrate/xxxx_create_api_changelogs.rb
class CreateApiChangelogs < ActiveRecord::Migration[7.1]
  def change
    create_table :api_changelogs do |t|
      t.string :version, null: false
      t.date :released_at, null: false
      t.string :change_type, null: false  # added, changed, deprecated, removed, fixed
      t.text :description, null: false
      t.boolean :breaking_change, default: false
 
      t.timestamps
    end
  end
end
 
# データ例
ApiChangelog.create!([
  {
    version: "2.1.0",
    released_at: Date.new(2024, 3, 1),
    change_type: "added",
    description: "`GET /users` に `status` フィルターを追加",
    breaking_change: false
  },
  {
    version: "2.0.0",
    released_at: Date.new(2024, 1, 1),
    change_type: "changed",
    description: "`username` フィールドを `name` に変更",
    breaking_change: true
  }
])

AWS S3 + CloudFrontでのドキュメントホスティング

# CloudFormation
DocsS3Bucket:
  Type: AWS::S3::Bucket
  Properties:
    BucketName: docs.techbridge.jp
    WebsiteConfiguration:
      IndexDocument: index.html
      ErrorDocument: 404.html
 
DocsCloudFrontDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      Origins:
        - DomainName: !Sub "${DocsS3Bucket}.s3-website-${AWS::Region}.amazonaws.com"
          Id: DocsOrigin
          CustomOriginConfig:
            HTTPPort: 80
            OriginProtocolPolicy: http-only
      DefaultCacheBehavior:
        ViewerProtocolPolicy: redirect-to-https
        CachePolicyId: !Ref DocsCachePolicy
        TargetOriginId: DocsOrigin
      Aliases:
        - docs.techbridge.jp
# ドキュメントのデプロイスクリプト
#!/bin/bash
rails rswag:specs:swaggerize  # OpenAPIドキュメント生成
 
# ドキュメントサイトをビルド
cd docs-site && npm run build
 
# S3にデプロイ
aws s3 sync docs-site/dist s3://docs.techbridge.jp --delete
 
# CloudFrontキャッシュを無効化
aws cloudfront create-invalidation \
  --distribution-id XXXXX \
  --paths "/*"

WARNING

ドキュメントは最高の営業ツールです。パートナー候補はドキュメントを見てAPIの品質を判断します。機能よりもドキュメントの完成度が採用の決め手になることさえあります。

サクラの達成感

数週間後、パートナー候補から返信が来た。

「ドキュメントが充実していて、Swagger UIで動作確認もできたので採用を決定しました」

「コードを書くのと同じくらい、ドキュメントを書くことが大切なんだ」

サクラはこれをチームのWikiに書いた:「ドキュメントはAPIの半分だ」。

次章では、APIの品質を保つテストとモニタリングを学ぶ。